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