Salome HOME
3df71fec024b518f2ea0ac060baca3d86fd475df
[tools/sat.git] / commands / config.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2012  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 import os
20 import sys
21 import platform
22 import datetime
23 import shutil
24 import gettext
25 import pprint as PP
26
27 import src
28 import src.logger as LOG
29 import src.debug as DBG
30 import src.callerName as CALN
31
32 logger = LOG.getDefaultLogger()
33
34 verbose = False # True for debug
35
36 # internationalization
37 satdir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
38 gettext.install('salomeTools', os.path.join(satdir, 'src', 'i18n'))
39
40 # Define all possible option for config command :  sat config <options>
41 parser = src.options.Options()
42 parser.add_option('v', 'value', 'string', 'value',
43     _("Optional: print the value of CONFIG_VARIABLE."))
44 parser.add_option('g', 'debug', 'string', 'debug',
45     _("Optional: print the debugging mode value of CONFIG_VARIABLE."))
46 parser.add_option('e', 'edit', 'boolean', 'edit',
47     _("Optional: edit the product configuration file."))
48 parser.add_option('i', 'info', 'list2', 'info',
49     _("Optional: get information on product(s). This option accepts a comma separated list."))
50 parser.add_option('p', 'products', 'list2', 'products',
51     _("Optional: same as --info, for convenience."))
52 parser.add_option('l', 'list', 'boolean', 'list',
53     _("Optional: list all available applications."))
54 parser.add_option('', 'show_patchs', 'boolean', 'show_patchs',
55     _("Optional: synthetic list of all patches used in the application"))
56 parser.add_option('', 'show_properties', 'boolean', 'show_properties',
57     _("Optional: synthetic list of all properties used in the application"))
58 parser.add_option('c', 'copy', 'boolean', 'copy',
59     _("""Optional: copy a config file to the personal config files directory.
60 WARNING: the included files are not copied.
61 If a name is given the new config file takes the given name."""))
62 parser.add_option('n', 'no_label', 'boolean', 'no_label',
63     _("Internal use: do not print labels, Works only with --value and --list."))
64 parser.add_option('', 'completion', 'boolean', 'completion',
65     _("Internal use: print only keys, works only with --value."))
66 parser.add_option('s', 'schema', 'boolean', 'schema',
67     _("Internal use."))
68
69 def osJoin(*args):
70   """
71   shortcut wrapper to os.path.join
72   plus optionaly print for debug
73   """
74   res = os.path.realpath(os.path.join(*args))
75   if verbose:
76     if True: # ".pyconf" in res:
77       logger.info("osJoin %-80s in %s" % (res, CALN.caller_name(1)))
78   return res
79
80 class ConfigOpener:
81     '''Class that helps to find an application pyconf 
82        in all the possible directories (pathList)
83     '''
84     def __init__(self, pathList):
85         '''Initialization
86         
87         :param pathList list: The list of paths where to search a pyconf.
88         '''
89         self.pathList = pathList
90         if verbose:
91           for path in pathList:
92             if not os.path.isdir(path):
93               logger.warning("ConfigOpener inexisting directory: %s" % path)
94
95     def __call__(self, name):
96         if os.path.isabs(name):
97             return src.pyconf.ConfigInputStream(open(name, 'rb'))
98         else:
99             return src.pyconf.ConfigInputStream(open(osJoin(self.get_path(name), name), 'rb'))
100         raise IOError(_("Configuration file '%s' not found") % name)
101
102     def get_path( self, name ):
103         '''The method that returns the entire path of the pyconf searched
104         returns first found in self.pathList directories
105
106         :param name str: The name of the searched pyconf.
107         '''
108         for path in self.pathList:
109             if os.path.exists(osJoin(path, name)):
110                 return path
111         raise IOError(_("Configuration file '%s' not found") % name)
112
113 class ConfigManager:
114     '''Class that manages the read of all the configuration files of salomeTools
115     '''
116     def __init__(self, datadir=None):
117         pass
118
119     def _create_vars(self, application=None, command=None, datadir=None):
120         '''Create a dictionary that stores all information about machine,
121            user, date, repositories, etc...
122         
123         :param application str: The application for which salomeTools is called.
124         :param command str: The command that is called.
125         :param datadir str: The repository that contain external data 
126                             for salomeTools.
127         :return: The dictionary that stores all information.
128         :rtype: dict
129         '''
130         var = {}      
131         var['user'] = src.architecture.get_user()
132         var['salometoolsway'] = os.path.dirname( os.path.dirname(os.path.abspath(__file__)))
133         var['srcDir'] =  osJoin(var['salometoolsway'], 'src')
134         var['internal_dir'] =  osJoin(var['srcDir'], 'internal_config')
135         var['sep']= os.path.sep
136         
137         # datadir has a default location
138         var['datadir'] =  osJoin(var['salometoolsway'], 'data')
139         if datadir is not None:
140             var['datadir'] = datadir
141
142         var['personalDir'] =  osJoin(os.path.expanduser('~'), '.salomeTools')
143         src.ensure_path_exists(var['personalDir'])
144
145         var['personal_applications_dir'] =  osJoin(var['personalDir'], "Applications")
146         src.ensure_path_exists(var['personal_applications_dir'])
147         
148         var['personal_products_dir'] =  osJoin(var['personalDir'], "products")
149         src.ensure_path_exists(var['personal_products_dir'])
150         
151         var['personal_archives_dir'] =  osJoin(var['personalDir'], "Archives")
152         src.ensure_path_exists(var['personal_archives_dir'])
153
154         var['personal_jobs_dir'] =  osJoin(var['personalDir'], "Jobs")
155         src.ensure_path_exists(var['personal_jobs_dir'])
156
157         var['personal_machines_dir'] =  osJoin(var['personalDir'], "Machines")
158         src.ensure_path_exists(var['personal_machines_dir'])
159
160         # read linux distributions dictionary
161         distrib_cfg = src.pyconf.Config( osJoin(var['srcDir'], 'internal_config', 'distrib.pyconf'))
162         
163         # set platform parameters
164         dist_name = src.architecture.get_distribution(codes=distrib_cfg.DISTRIBUTIONS)
165         dist_version = src.architecture.get_distrib_version(dist_name)
166         dist_version_full = src.architecture.get_version_XY()
167         dist = dist_name + dist_version
168         
169         var['dist_name'] = dist_name
170         var['dist_version'] = dist_version
171         var['dist'] = dist
172         var['dist_ref'] = dist_name + dist_version_full
173         var['python'] = src.architecture.get_python_version()
174
175         var['nb_proc'] = src.architecture.get_nb_proc()
176         node_name = platform.node()
177         var['node'] = node_name
178         var['hostname'] = node_name
179
180         # set date parameters
181         dt = datetime.datetime.now()
182         var['date'] = dt.strftime('%Y%m%d')
183         var['datehour'] = dt.strftime('%Y%m%d_%H%M%S')
184         var['hour'] = dt.strftime('%H%M%S')
185
186         var['command'] = str(command)
187         var['application'] = str(application)
188
189         # Root dir for temporary files 
190         var['tmp_root'] = os.sep + 'tmp' + os.sep + var['user']
191         # particular win case 
192         if src.architecture.is_windows() : 
193             var['tmp_root'] =  os.path.expanduser('~') + os.sep + 'tmp'
194         
195         return var
196
197     def get_command_line_overrides(self, options, sections):
198         '''get all the overwrites that are in the command line
199         
200         :param options: the options from salomeTools class 
201                         initialization (like -l5 or --overwrite)
202         :param sections str: The config section to overwrite.
203         :return: The list of all the overwrites to apply.
204         :rtype: list
205         '''
206         # when there are no options or not the overwrite option, 
207         # return an empty list
208         if options is None or options.overwrite is None:
209             return []
210         
211         over = []
212         for section in sections:
213             # only overwrite the sections that correspond to the option 
214             over.extend(filter(lambda l: l.startswith(section + "."), 
215                                options.overwrite))
216         return over
217
218     def get_config(self, application=None, options=None, command=None,
219                     datadir=None):
220         '''get the config from all the configuration files.
221         
222         :param application str: The application for which salomeTools is called.
223         :param options class Options: The general salomeToos
224                                       options (--overwrite or -l5, for example)
225         :param command str: The command that is called.
226         :param datadir str: The repository that contain 
227                             external data for salomeTools.
228         :return: The final config.
229         :rtype: class 'src.pyconf.Config'
230         '''        
231         
232         # create a ConfigMerger to handle merge
233         merger = src.pyconf.ConfigMerger()#MergeHandler())
234         
235         # create the configuration instance
236         cfg = src.pyconf.Config()
237         
238         # =====================================================================
239         # create VARS section
240         var = self._create_vars(application=application, command=command, datadir=datadir)
241         # DBG.write("create_vars", var, DBG.isDeveloper())
242
243         # add VARS to config
244         cfg.VARS = src.pyconf.Mapping(cfg)
245         for variable in var:
246             cfg.VARS[variable] = var[variable]
247         
248         # apply overwrite from command line if needed
249         for rule in self.get_command_line_overrides(options, ["VARS"]):
250             exec('cfg.' + rule) # this cannot be factorized because of the exec
251         
252         # =====================================================================
253         # Load INTERNAL config
254         # read src/internal_config/salomeTools.pyconf
255         src.pyconf.streamOpener = ConfigOpener([
256                              osJoin(cfg.VARS.srcDir, 'internal_config')])
257         try:
258             internal_cfg = src.pyconf.Config(open( osJoin(cfg.VARS.srcDir,
259                                     'internal_config', 'salomeTools.pyconf')))
260         except src.pyconf.ConfigError as e:
261             raise src.SatException(_("Error in configuration file:"
262                                      " salomeTools.pyconf\n  %(error)s") % \
263                                    {'error': str(e) })
264         
265         merger.merge(cfg, internal_cfg)
266
267         # apply overwrite from command line if needed
268         for rule in self.get_command_line_overrides(options, ["INTERNAL"]):
269             exec('cfg.' + rule) # this cannot be factorized because of the exec        
270                
271         # =====================================================================
272         # Load LOCAL config file
273         # search only in the data directory
274         src.pyconf.streamOpener = ConfigOpener([cfg.VARS.datadir])
275         try:
276             local_cfg = src.pyconf.Config(open( osJoin(cfg.VARS.datadir,
277                                                            'local.pyconf')),
278                                          PWD = ('LOCAL', cfg.VARS.datadir) )
279         except src.pyconf.ConfigError as e:
280             raise src.SatException(_("Error in configuration file: "
281                                      "local.pyconf\n  %(error)s") % \
282                 {'error': str(e) })
283         except IOError as error:
284             e = str(error)
285             raise src.SatException( e );
286         merger.merge(cfg, local_cfg)
287
288         # When the key is "default", put the default value
289         if cfg.LOCAL.base == "default":
290             cfg.LOCAL.base = os.path.abspath(osJoin(cfg.VARS.salometoolsway, "..", "BASE"))
291         if cfg.LOCAL.workdir == "default":
292             cfg.LOCAL.workdir = os.path.abspath(osJoin(cfg.VARS.salometoolsway, ".."))
293         if cfg.LOCAL.log_dir == "default":
294             cfg.LOCAL.log_dir = os.path.abspath(osJoin(cfg.VARS.salometoolsway, "..", "LOGS"))
295
296         if cfg.LOCAL.archive_dir == "default":
297             cfg.LOCAL.archive_dir = os.path.abspath( osJoin(cfg.VARS.salometoolsway, "..", "ARCHIVES"))
298
299         # if the sat tag was not set permanently by user
300         if cfg.LOCAL.tag == None:
301             # get the tag with git, and store it
302             sat_version=src.system.git_describe(cfg.VARS.salometoolsway) 
303             if sat_version == False:
304                 sat_version=cfg.INTERNAL.sat_version
305             cfg.LOCAL.tag=sat_version
306                 
307
308         # apply overwrite from command line if needed
309         for rule in self.get_command_line_overrides(options, ["LOCAL"]):
310             exec('cfg.' + rule) # this cannot be factorized because of the exec
311         
312         # =====================================================================
313         # Load the PROJECTS
314         projects_cfg = src.pyconf.Config()
315         projects_cfg.addMapping("PROJECTS",
316                                 src.pyconf.Mapping(projects_cfg),
317                                 "The projects\n")
318         projects_cfg.PROJECTS.addMapping("projects",
319                                 src.pyconf.Mapping(cfg.PROJECTS),
320                                 "The projects definition\n")
321         
322         for project_pyconf_path in cfg.PROJECTS.project_file_paths:
323             if not os.path.isabs(project_pyconf_path):
324                 # for a relative path (archive case) we complete with sat path
325                 project_pyconf_path = os.path.join(cfg.VARS.salometoolsway,
326                                                   project_pyconf_path)
327             if not os.path.exists(project_pyconf_path):
328                 msg = _("WARNING: The project file %s cannot be found. "
329                         "It will be ignored\n" % project_pyconf_path)
330                 sys.stdout.write(msg)
331                 continue
332             project_name = os.path.basename(
333                                     project_pyconf_path)[:-len(".pyconf")]
334             try:
335                 project_pyconf_dir = os.path.dirname(project_pyconf_path)
336                 project_cfg = src.pyconf.Config(open(project_pyconf_path),
337                                                 PWD=("", project_pyconf_dir))
338             except Exception as e:
339                 msg = _("ERROR: Error in configuration file: "
340                                  "%(file_path)s\n  %(error)s\n") % \
341                             {'file_path' : project_pyconf_path, 'error': str(e) }
342                 sys.stdout.write(msg)
343                 continue
344             projects_cfg.PROJECTS.projects.addMapping(project_name,
345                              src.pyconf.Mapping(projects_cfg.PROJECTS.projects),
346                              "The %s project\n" % project_name)
347             projects_cfg.PROJECTS.projects[project_name]=project_cfg
348             projects_cfg.PROJECTS.projects[project_name]["file_path"] = \
349                                                         project_pyconf_path
350             # store the project tag if any
351             product_project_git_tag = src.system.git_describe(os.path.dirname(project_pyconf_path))
352             if product_project_git_tag:
353                 projects_cfg.PROJECTS.projects[project_name]["git_tag"] = product_project_git_tag
354             else:
355                 projects_cfg.PROJECTS.projects[project_name]["git_tag"] = "unknown"
356                    
357         merger.merge(cfg, projects_cfg)
358
359         # apply overwrite from command line if needed
360         for rule in self.get_command_line_overrides(options, ["PROJECTS"]):
361             exec('cfg.' + rule) # this cannot be factorized because of the exec
362         
363         # =====================================================================
364         # Create the paths where to search the application configurations, 
365         # the product configurations, the products archives, 
366         # the jobs configurations and the machines configurations
367         cfg.addMapping("PATHS", src.pyconf.Mapping(cfg), "The paths\n")
368         cfg.PATHS["APPLICATIONPATH"] = src.pyconf.Sequence(cfg.PATHS)
369         cfg.PATHS.APPLICATIONPATH.append(cfg.VARS.personal_applications_dir, "")
370
371         
372         cfg.PATHS["PRODUCTPATH"] = src.pyconf.Sequence(cfg.PATHS)
373         cfg.PATHS.PRODUCTPATH.append(cfg.VARS.personal_products_dir, "")
374         cfg.PATHS["ARCHIVEPATH"] = src.pyconf.Sequence(cfg.PATHS)
375         cfg.PATHS.ARCHIVEPATH.append(cfg.VARS.personal_archives_dir, "")
376         cfg.PATHS["ARCHIVEFTP"] = src.pyconf.Sequence(cfg.PATHS)
377         cfg.PATHS["JOBPATH"] = src.pyconf.Sequence(cfg.PATHS)
378         cfg.PATHS.JOBPATH.append(cfg.VARS.personal_jobs_dir, "")
379         cfg.PATHS["MACHINEPATH"] = src.pyconf.Sequence(cfg.PATHS)
380         cfg.PATHS.MACHINEPATH.append(cfg.VARS.personal_machines_dir, "")
381         cfg.PATHS["LICENCEPATH"] = src.pyconf.Sequence(cfg.PATHS)
382
383         # initialise the path with local directory
384         cfg.PATHS["ARCHIVEPATH"].append(cfg.LOCAL.archive_dir, "")
385
386         # Loop over the projects in order to complete the PATHS variables
387         # as /data/tmpsalome/salome/prerequis/archives for example ARCHIVEPATH
388         for project in cfg.PROJECTS.projects:
389             for PATH in ["APPLICATIONPATH",
390                          "PRODUCTPATH",
391                          "ARCHIVEPATH", #comment this for default archive       #8646
392                          "ARCHIVEFTP",
393                          "JOBPATH",
394                          "MACHINEPATH",
395                          "LICENCEPATH"]:
396                 if PATH not in cfg.PROJECTS.projects[project]:
397                     continue
398                 cfg.PATHS[PATH].append(cfg.PROJECTS.projects[project][PATH], "")
399         
400         # apply overwrite from command line if needed
401         for rule in self.get_command_line_overrides(options, ["PATHS"]):
402             exec('cfg.' + rule) # this cannot be factorized because of the exec
403
404         # AT END append APPLI_TEST directory in APPLICATIONPATH, for unittest
405         appli_test_dir =  osJoin(satdir, "test", "APPLI_TEST")
406         if appli_test_dir not in cfg.PATHS.APPLICATIONPATH:
407           cfg.PATHS.APPLICATIONPATH.append(appli_test_dir, "unittest APPLI_TEST path")
408
409         # =====================================================================
410         # Load APPLICATION config file
411         if application is not None:
412             # search APPLICATION file in all directories in configPath
413             cp = cfg.PATHS.APPLICATIONPATH
414             src.pyconf.streamOpener = ConfigOpener(cp)
415             do_merge = True
416             try:
417                 application_cfg = src.pyconf.Config(application + '.pyconf')
418             except IOError as e:
419                 raise src.SatException(
420                    _("%s, use 'config --list' to get the list of available applications.") % e)
421             except src.pyconf.ConfigError as e:
422                 if (not ('-e' in parser.parse_args()[1]) 
423                                          or ('--edit' in parser.parse_args()[1]) 
424                                          and command == 'config'):
425                     raise src.SatException(_("Error in configuration file: "
426                                              "%(application)s.pyconf\n "
427                                              " %(error)s") % \
428                         { 'application': application, 'error': str(e) } )
429                 else:
430                     sys.stdout.write(src.printcolors.printcWarning(
431                                         "There is an error in the file"
432                                         " %s.pyconf.\n" % cfg.VARS.application))
433                     do_merge = False
434             except Exception as e:
435                 if (not ('-e' in parser.parse_args()[1]) 
436                                         or ('--edit' in parser.parse_args()[1]) 
437                                         and command == 'config'):
438                     sys.stdout.write(src.printcolors.printcWarning("%s\n" % str(e)))
439                     raise src.SatException(_("Error in configuration file:"
440                                              " %(application)s.pyconf\n") % \
441                         { 'application': application} )
442                 else:
443                     sys.stdout.write(src.printcolors.printcWarning(
444                                 "There is an error in the file"
445                                 " %s.pyconf. Opening the file with the"
446                                 " default viewer\n" % cfg.VARS.application))
447                     sys.stdout.write("The error:"
448                                  " %s\n" % src.printcolors.printcWarning(
449                                                                       str(e)))
450                     do_merge = False
451         
452             else:
453                 cfg['open_application'] = 'yes'
454
455         # =====================================================================
456         # Load product config files in PRODUCTS section
457         products_cfg = src.pyconf.Config()
458         products_cfg.addMapping("PRODUCTS",
459                                 src.pyconf.Mapping(products_cfg),
460                                 "The products\n")
461         if application is not None:
462             src.pyconf.streamOpener = ConfigOpener(cfg.PATHS.PRODUCTPATH)
463             for product_name in application_cfg.APPLICATION.products.keys():
464                 # Loop on all files that are in softsDir directory
465                 # and read their config
466                 product_file_name = product_name + ".pyconf"
467                 product_file_path = src.find_file_in_lpath(product_file_name, cfg.PATHS.PRODUCTPATH)
468                 if product_file_path:
469                     products_dir = os.path.dirname(product_file_path)
470                     # for a relative path (archive case) we complete with sat path
471                     if not os.path.isabs(products_dir):
472                         products_dir = os.path.join(cfg.VARS.salometoolsway,
473                                                     products_dir)
474                     try:
475                         prod_cfg = src.pyconf.Config(open(product_file_path),
476                                                      PWD=("", products_dir))
477                         prod_cfg.from_file = product_file_path
478                         products_cfg.PRODUCTS[product_name] = prod_cfg
479                     except Exception as e:
480                         msg = _(
481                             "WARNING: Error in configuration file"
482                             ": %(prod)s\n  %(error)s" % \
483                             {'prod' :  product_name, 'error': str(e) })
484                         sys.stdout.write(msg)
485             
486             merger.merge(cfg, products_cfg)
487             
488             # apply overwrite from command line if needed
489             for rule in self.get_command_line_overrides(options, ["PRODUCTS"]):
490                 exec('cfg.' + rule) # this cannot be factorized because of the exec
491             
492             if do_merge:
493                 merger.merge(cfg, application_cfg)
494
495                 # default launcher name ('salome')
496                 if ('profile' in cfg.APPLICATION and 
497                     'launcher_name' not in cfg.APPLICATION.profile):
498                     cfg.APPLICATION.profile.launcher_name = 'salome'
499
500                 # apply overwrite from command line if needed
501                 for rule in self.get_command_line_overrides(options,
502                                                              ["APPLICATION"]):
503                     # this cannot be factorized because of the exec
504                     exec('cfg.' + rule)
505             
506         # =====================================================================
507         # load USER config
508         self.set_user_config_file(cfg)
509         user_cfg_file = self.get_user_config_file()
510         user_cfg = src.pyconf.Config(open(user_cfg_file))
511         merger.merge(cfg, user_cfg)
512
513         # apply overwrite from command line if needed
514         for rule in self.get_command_line_overrides(options, ["USER"]):
515             exec('cfg.' + rule) # this cannot be factorize because of the exec
516         
517         return cfg
518
519     def set_user_config_file(self, config):
520         '''Set the user config file name and path.
521         If necessary, build it from another one or create it from scratch.
522         
523         :param config class 'src.pyconf.Config': The global config 
524                                                  (containing all pyconf).
525         '''
526         # get the expected name and path of the file
527         self.config_file_name = 'SAT.pyconf'
528         self.user_config_file_path =  osJoin(config.VARS.personalDir, self.config_file_name)
529         
530         # if pyconf does not exist, create it from scratch
531         if not os.path.isfile(self.user_config_file_path): 
532             self.create_config_file(config)
533     
534     def create_config_file(self, config):
535         '''This method is called when there are no user config file. 
536            It build it from scratch.
537         
538         :param config class 'src.pyconf.Config': The global config.
539         :return: the config corresponding to the file created.
540         :rtype: config class 'src.pyconf.Config'
541         '''
542         
543         cfg_name = self.get_user_config_file()
544
545         user_cfg = src.pyconf.Config()
546         #
547         user_cfg.addMapping('USER', src.pyconf.Mapping(user_cfg), "")
548
549         user_cfg.USER.addMapping('cvs_user', config.VARS.user,
550             "This is the user name used to access salome cvs base.\n")
551         user_cfg.USER.addMapping('svn_user', config.VARS.user,
552             "This is the user name used to access salome svn base.\n")
553         user_cfg.USER.addMapping('output_verbose_level', 3,
554             "This is the default output_verbose_level you want."
555             " 0=>no output, 5=>debug.\n")
556         user_cfg.USER.addMapping('publish_dir', 
557                                   osJoin(os.path.expanduser('~'),
558                                  'websupport', 
559                                  'satreport'), 
560                                  "")
561         user_cfg.USER.addMapping('editor',
562                                  'vi', 
563                                  "This is the editor used to "
564                                  "modify configuration files\n")
565         user_cfg.USER.addMapping('browser', 
566                                  'firefox', 
567                                  "This is the browser used to "
568                                  "read html documentation\n")
569         user_cfg.USER.addMapping('pdf_viewer', 
570                                  'evince', 
571                                  "This is the pdf_viewer used "
572                                  "to read pdf documentation\n")
573 # CNC 25/10/17 : plus nécessaire a priori
574 #        user_cfg.USER.addMapping("base",
575 #                                 src.pyconf.Reference(
576 #                                            user_cfg,
577 #                                            src.pyconf.DOLLAR,
578 #                                            'workdir  + $VARS.sep + "BASE"'),
579 #                                 "The products installation base (could be "
580 #                                 "ignored if this key exists in the local.pyconf"
581 #                                 " file of salomTools).\n")
582
583         src.ensure_path_exists(config.VARS.personalDir)
584         src.ensure_path_exists( osJoin(config.VARS.personalDir,
585                                             'Applications'))
586
587         f = open(cfg_name, 'w')
588         user_cfg.__save__(f)
589         f.close()
590
591         return user_cfg   
592
593     def get_user_config_file(self):
594         '''Get the user config file
595         :return: path to the user config file.
596         :rtype: str
597         '''
598         if not self.user_config_file_path:
599             raise src.SatException(_("Error in get_user_config_file: "
600                                      "missing user config file path"))
601         return self.user_config_file_path     
602
603 def check_path(path, ext=[]):
604     '''Construct a text with the input path and "not found" if it does not
605        exist.
606     
607     :param path Str: the path to check.
608     :param ext List: An extension. Verify that the path extension 
609                      is in the list
610     :return: The string of the path with information
611     :rtype: Str
612     '''
613     # check if file exists
614     if not os.path.exists(path):
615         return "'%s'" % path + " " + src.printcolors.printcError(_(
616                                                             "** not found"))
617
618     # check extension
619     if len(ext) > 0:
620         fe = os.path.splitext(path)[1].lower()
621         if fe not in ext:
622             return "'%s'" % path + " " + src.printcolors.printcError(_(
623                                                         "** bad extension"))
624
625     return path
626
627 def show_product_info(config, name, logger):
628     '''Display on the terminal and logger information about a product.
629     
630     :param config Config: the global configuration.
631     :param name Str: The name of the product
632     :param logger Logger: The logger instance to use for the display
633     '''
634     
635     logger.write(_("%s is a product\n") % src.printcolors.printcLabel(name), 2)
636     pinfo = src.product.get_product_config(config, name)
637     
638     if "depend" in pinfo:
639         src.printcolors.print_value(logger, "depends on", sorted(pinfo.depend), 2)
640
641     if "opt_depend" in pinfo:
642         src.printcolors.print_value(logger, "optional", sorted(pinfo.opt_depend), 2)
643
644     # information on pyconf
645     logger.write("\n", 2)
646     logger.write(src.printcolors.printcLabel("configuration:") + "\n", 2)
647     if "from_file" in pinfo:
648         src.printcolors.print_value(logger,
649                                     "pyconf file path",
650                                     pinfo.from_file,
651                                     2)
652     if "section" in pinfo:
653         src.printcolors.print_value(logger,
654                                     "section",
655                                     pinfo.section,
656                                     2)
657
658     # information on prepare
659     logger.write("\n", 2)
660     logger.write(src.printcolors.printcLabel("prepare:") + "\n", 2)
661
662     is_dev = src.product.product_is_dev(pinfo)
663     method = pinfo.get_source
664     if is_dev:
665         method += " (dev)"
666     src.printcolors.print_value(logger, "get method", method, 2)
667
668     if method == 'cvs':
669         src.printcolors.print_value(logger, "server", pinfo.cvs_info.server, 2)
670         src.printcolors.print_value(logger, "base module",
671                                     pinfo.cvs_info.module_base, 2)
672         src.printcolors.print_value(logger, "source", pinfo.cvs_info.source, 2)
673         src.printcolors.print_value(logger, "tag", pinfo.cvs_info.tag, 2)
674
675     elif method == 'svn':
676         src.printcolors.print_value(logger, "repo", pinfo.svn_info.repo, 2)
677
678     elif method == 'git':
679         src.printcolors.print_value(logger, "repo", pinfo.git_info.repo, 2)
680         src.printcolors.print_value(logger, "tag", pinfo.git_info.tag, 2)
681
682     elif method == 'archive':
683         src.printcolors.print_value(logger,
684                                     "get from",
685                                     check_path(pinfo.archive_info.archive_name),
686                                     2)
687
688     if 'patches' in pinfo:
689         for patch in pinfo.patches:
690             src.printcolors.print_value(logger, "patch", check_path(patch), 2)
691
692     if src.product.product_is_fixed(pinfo):
693         src.printcolors.print_value(logger, "install_dir",
694                                     check_path(pinfo.install_dir), 2)
695
696     if src.product.product_is_native(pinfo) or src.product.product_is_fixed(pinfo):
697         return
698     
699     # information on compilation
700     if src.product.product_compiles(pinfo):
701         logger.write("\n", 2)
702         logger.write(src.printcolors.printcLabel("compile:") + "\n", 2)
703         src.printcolors.print_value(logger,
704                                     "compilation method",
705                                     pinfo.build_source,
706                                     2)
707         
708         if pinfo.build_source == "script" and "compil_script" in pinfo:
709             src.printcolors.print_value(logger, 
710                                         "Compilation script", 
711                                         pinfo.compil_script, 
712                                         2)
713         
714         if 'nb_proc' in pinfo:
715             src.printcolors.print_value(logger, "make -j", pinfo.nb_proc, 2)
716     
717         src.printcolors.print_value(logger, 
718                                     "source dir", 
719                                     check_path(pinfo.source_dir), 
720                                     2)
721         if 'install_dir' in pinfo:
722             src.printcolors.print_value(logger, 
723                                         "build dir", 
724                                         check_path(pinfo.build_dir), 
725                                         2)
726             src.printcolors.print_value(logger, 
727                                         "install dir", 
728                                         check_path(pinfo.install_dir), 
729                                         2)
730         else:
731             logger.write("  " + 
732                          src.printcolors.printcWarning(_("no install dir")) + 
733                          "\n", 2)
734     else:
735         logger.write("\n", 2)
736         msg = _("This product does not compile")
737         logger.write("%s\n" % msg, 2)
738
739     # information on environment
740     logger.write("\n", 2)
741     logger.write(src.printcolors.printcLabel("environ :") + "\n", 2)
742     if "environ" in pinfo and "env_script" in pinfo.environ:
743         src.printcolors.print_value(logger, 
744                                     "script", 
745                                     check_path(pinfo.environ.env_script), 
746                                     2)
747
748     # display run-time environment
749     zz = src.environment.SalomeEnviron(config,
750                                        src.fileEnviron.ScreenEnviron(logger), 
751                                        False)
752     zz.set_python_libdirs()
753     zz.set_a_product(name, logger)
754     logger.write("\n", 2)
755
756
757 def show_patchs(config, logger):
758   '''Prints all the used patchs in the application.
759
760   :param config Config: the global configuration.
761   :param logger Logger: The logger instance to use for the display
762   '''
763   oneOrMore = False
764   for product in sorted(config.APPLICATION.products):
765     try:
766       product_info = src.product.get_product_config(config, product)
767       if src.product.product_has_patches(product_info):
768         oneOrMore = True
769         logger.write("%s:\n" % product, 1)
770         for i in product_info.patches:
771           logger.write(src.printcolors.printcInfo("    %s\n" % i), 1)
772     except Exception as e:
773       msg = "problem on product %s\n%s\n" % (product, str(e))
774       logger.error(msg)
775
776   if oneOrMore:
777     logger.write("\n", 1)
778   else:
779     logger.write("No patchs found\n", 1)
780
781
782 def show_properties(config, logger):
783   '''Prints all the used properties in the application.
784
785   :param config Config: the global configuration.
786   :param logger Logger: The logger instance to use for the display
787   '''
788   oneOrMore = False
789   for product in sorted(config.APPLICATION.products):
790     try:
791       product_info = src.product.get_product_config(config, product)
792       done = False
793       try:
794         for i in product_info.properties:
795           if not done:
796             logger.write("%s:\n" % product, 1)
797             done = True
798           oneOrMore = True
799           logger.write(src.printcolors.printcInfo("    %s\n" % i), 1)
800       except Exception as e:
801         pass
802     except Exception as e:
803       # logger.write(src.printcolors.printcInfo("    %s\n" % "no properties"), 1)
804       msg = "problem on product %s\n%s\n" % (product, e)
805       logger.error(msg)
806
807   if oneOrMore:
808     logger.write("\n", 1)
809   else:
810     logger.write("No properties found\n", 1)
811
812 def print_value(config, path, show_label, logger, level=0, show_full_path=False):
813     '''Prints a value from the configuration. Prints recursively the values 
814        under the initial path.
815     
816     :param config class 'src.pyconf.Config': The configuration 
817                                              from which the value is displayed.
818     :param path str : the path in the configuration of the value to print.
819     :param show_label boolean: if True, do a basic display. 
820                                (useful for bash completion)
821     :param logger Logger: the logger instance
822     :param level int: The number of spaces to add before display.
823     :param show_full_path :
824     '''            
825     
826     # Make sure that the path does not ends with a point
827     if path.endswith('.'):
828         path = path[:-1]
829     
830     # display all the path or not
831     if show_full_path:
832         vname = path
833     else:
834         vname = path.split('.')[-1]
835
836     # number of spaces before the display
837     tab_level = "  " * level
838     
839     # call to the function that gets the value of the path.
840     try:
841         val = config.getByPath(path)
842     except Exception as e:
843         logger.write(tab_level)
844         logger.write("%s: ERROR %s\n" % (src.printcolors.printcLabel(vname), 
845                                          src.printcolors.printcError(str(e))))
846         return
847
848     # in this case, display only the value
849     if show_label:
850         logger.write(tab_level)
851         logger.write("%s: " % src.printcolors.printcLabel(vname))
852
853     # The case where the value has under values, 
854     # do a recursive call to the function
855     if dir(val).__contains__('keys'):
856         if show_label: logger.write("\n")
857         for v in sorted(val.keys()):
858             print_value(config, path + '.' + v, show_label, logger, level + 1)
859     elif val.__class__ == src.pyconf.Sequence or isinstance(val, list): 
860         # in this case, value is a list (or a Sequence)
861         if show_label: logger.write("\n")
862         index = 0
863         for v in val:
864             print_value(config, path + "[" + str(index) + "]", 
865                         show_label, logger, level + 1)
866             index = index + 1
867     else: # case where val is just a str
868         logger.write("%s\n" % val)
869
870 def get_config_children(config, args):
871     '''Gets the names of the children of the given parameter.
872        Useful only for completion mechanism
873     
874     :param config Config: The configuration where to read the values
875     :param args: The path in the config from which get the keys
876     '''
877     vals = []
878     rootkeys = config.keys()
879     
880     if len(args) == 0:
881         # no parameter returns list of root keys
882         vals = rootkeys
883     else:
884         parent = args[0]
885         pos = parent.rfind('.')
886         if pos < 0:
887             # Case where there is only on key as parameter.
888             # For example VARS
889             vals = [m for m in rootkeys if m.startswith(parent)]
890         else:
891             # Case where there is a part from a key
892             # for example VARS.us  (for VARS.user)
893             head = parent[0:pos]
894             tail = parent[pos+1:]
895             try:
896                 a = config.getByPath(head)
897                 if dir(a).__contains__('keys'):
898                     vals = map(lambda x: head + '.' + x,
899                                [m for m in a.keys() if m.startswith(tail)])
900             except:
901                 pass
902
903     for v in sorted(vals):
904         sys.stdout.write("%s\n" % v)
905
906 def description():
907     '''method that is called when salomeTools is called with --help option.
908     
909     :return: The text to display for the config command description.
910     :rtype: str
911     '''
912     return _("The config command allows manipulation "
913              "and operation on config files.\n\nexample:\nsat config "
914              "SALOME-master --info ParaView")
915     
916
917 def run(args, runner, logger):
918     '''method that is called when salomeTools is called with config parameter.
919     '''
920     # Parse the options
921     (options, args) = parser.parse_args(args)
922
923     # Only useful for completion mechanism : print the keys of the config
924     if options.schema:
925         get_config_children(runner.cfg, args)
926         return
927
928     # case : print a value of the config
929     if options.value:
930         if options.value == ".":
931             # if argument is ".", print all the config
932             for val in sorted(runner.cfg.keys()):
933                 print_value(runner.cfg, val, not options.no_label, logger)
934         else:
935             print_value(runner.cfg, options.value, not options.no_label, logger, 
936                         level=0, show_full_path=False)
937     
938     # case : print a debug value of the config
939     if options.debug:
940         if options.debug == ".":
941             # if argument is ".", print all the config
942             res = DBG.indent(DBG.getStrConfigDbg(runner.cfg))
943             logger.write("\nConfig of application %s:\n\n%s\n" % (runner.cfg.VARS.application, res))
944         else:
945             if options.debug[0] == ".": # accept ".PRODUCT.etc" as "PRODUCT.etc"
946               od = options.debug[1:]
947             else:
948               od = options.debug
949             try:
950               aCode = "a = runner.cfg.%s" % od
951               # https://stackoverflow.com/questions/15086040/behavior-of-exec-function-in-python-2-and-python-3
952               aDict = {"runner": runner}
953               exec(aCode, globals(), aDict)
954               # DBG.write("globals()", globals(), True)
955               # DBG.write("aDict", aDict, True)
956               res = DBG.indent(DBG.getStrConfigDbg(aDict["a"]))
957               logger.write("\nConfig.%s of application %s:\n\n%s\n" % (od, runner.cfg.VARS.application, res))
958             except Exception as e:
959               msg = "\nConfig.%s of application %s: Unknown pyconf key\n" % (od, runner.cfg.VARS.application)
960               logger.write(src.printcolors.printcError(msg), 1)
961
962     
963     # case : edit user pyconf file or application file
964     if options.edit:
965         editor = runner.cfg.USER.editor
966         if ('APPLICATION' not in runner.cfg and
967                        'open_application' not in runner.cfg): # edit user pyconf
968             usercfg =  osJoin(runner.cfg.VARS.personalDir,
969                                    'SAT.pyconf')
970             logger.write(_("Opening %s\n" % usercfg), 3)
971             src.system.show_in_editor(editor, usercfg, logger)
972         else:
973             # search for file <application>.pyconf and open it
974             for path in runner.cfg.PATHS.APPLICATIONPATH:
975                 pyconf_path =  osJoin(path,
976                                     runner.cfg.VARS.application + ".pyconf")
977                 if os.path.exists(pyconf_path):
978                     logger.write(_("Opening %s\n" % pyconf_path), 3)
979                     src.system.show_in_editor(editor, pyconf_path, logger)
980                     break
981     
982     # case : give information about the product(s) in parameter
983     if options.products:
984       if options.info is not None:
985         logger.warning('options.products %s overrides options.info %s' % (options.products, options.info))
986       options.info = options.products
987
988     if options.info:
989       # DBG.write("products", sorted(runner.cfg.APPLICATION.products.keys()), True)
990       src.check_config_has_application(runner.cfg)
991       taggedProducts = src.getProductNames(runner.cfg, options.info, logger)
992       DBG.write("tagged products", sorted(taggedProducts))
993       for prod in sorted(taggedProducts):
994         if prod in runner.cfg.APPLICATION.products:
995           try:
996             if len(taggedProducts) > 1:
997               logger.write("#################### ", 2)
998             show_product_info(runner.cfg, prod, logger)
999           except Exception as e:
1000             msg = "problem on product %s\n%s\n" % (prod, str(e))
1001             logger.error(msg)
1002           # return
1003         else:
1004           msg = _("%s is not a product of %s.\n") % \
1005                 (prod, runner.cfg.VARS.application)
1006           logger.warning(msg)
1007           #raise Exception(msg)
1008     
1009     # case : copy an existing <application>.pyconf 
1010     # to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
1011     if options.copy:
1012         # product is required
1013         src.check_config_has_application( runner.cfg )
1014
1015         # get application file path 
1016         source = runner.cfg.VARS.application + '.pyconf'
1017         source_full_path = ""
1018         for path in runner.cfg.PATHS.APPLICATIONPATH:
1019             # ignore personal directory
1020             if path == runner.cfg.VARS.personalDir:
1021                 continue
1022             # loop on all directories that can have pyconf applications
1023             zz =  osJoin(path, source)
1024             if os.path.exists(zz):
1025                 source_full_path = zz
1026                 break
1027
1028         if len(source_full_path) == 0:
1029             raise src.SatException(_(
1030                         "Config file for product %s not found\n") % source)
1031         else:
1032             if len(args) > 0:
1033                 # a name is given as parameter, use it
1034                 dest = args[0]
1035             elif 'copy_prefix' in runner.cfg.INTERNAL.config:
1036                 # use prefix
1037                 dest = (runner.cfg.INTERNAL.config.copy_prefix 
1038                         + runner.cfg.VARS.application)
1039             else:
1040                 # use same name as source
1041                 dest = runner.cfg.VARS.application
1042                 
1043             # the full path
1044             dest_file =  osJoin(runner.cfg.VARS.personalDir,
1045                                      'Applications', dest + '.pyconf')
1046             if os.path.exists(dest_file):
1047                 raise src.SatException(_("A personal application"
1048                                          " '%s' already exists") % dest)
1049             
1050             # perform the copy
1051             shutil.copyfile(source_full_path, dest_file)
1052             logger.write(_("%s has been created.\n") % dest_file)
1053     
1054     # case : display all the available pyconf applications
1055     if options.list:
1056         lproduct = list()
1057         # search in all directories that can have pyconf applications
1058         for path in runner.cfg.PATHS.APPLICATIONPATH:
1059             # print a header
1060             if not options.no_label:
1061                 logger.write("------ %s\n" % src.printcolors.printcHeader(path))
1062
1063             if not os.path.exists(path):
1064                 logger.write(src.printcolors.printcError(_(
1065                                             "Directory not found")) + "\n")
1066             else:
1067                 for f in sorted(os.listdir(path)):
1068                     # ignore file that does not ends with .pyconf
1069                     if not f.endswith('.pyconf'):
1070                         continue
1071
1072                     appliname = f[:-len('.pyconf')]
1073                     if appliname not in lproduct:
1074                         lproduct.append(appliname)
1075                         if path.startswith(runner.cfg.VARS.personalDir) \
1076                                     and not options.no_label:
1077                             logger.write("%s*\n" % appliname)
1078                         else:
1079                             logger.write("%s\n" % appliname)
1080                             
1081             logger.write("\n")
1082
1083     # case: print all the products name of the application (internal use for completion)
1084     if options.completion:
1085         for product_name in runner.cfg.APPLICATION.products.keys():
1086             logger.write("%s\n" % product_name)
1087         
1088     # case : give a synthetic view of all patches used in the application
1089     if options.show_patchs:
1090         src.check_config_has_application(runner.cfg)
1091         # Print some informations
1092         logger.write(_('Patchs of application %s\n') %
1093                     src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
1094         logger.write("\n", 2, False)
1095         show_patchs(runner.cfg, logger)
1096
1097     # case : give a synthetic view of all patches used in the application
1098     if options.show_properties:
1099         src.check_config_has_application(runner.cfg)
1100         # Print some informations
1101         logger.write(_('Properties of application %s\n') %
1102                     src.printcolors.printcLabel(runner.cfg.VARS.application), 3)
1103         logger.write("\n", 2, False)
1104         show_properties(runner.cfg, logger)
1105