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