Salome HOME
git: ignore VSCode cache files, sat cache file and add a pre-commit config
[tools/sat.git] / commands / application.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2013  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 stat
21 import sys
22 import shutil
23 import subprocess
24 import getpass
25
26 from src import ElementTree as etree
27 import src
28 from  src.versionMinorMajorPatch import MinorMajorPatch as MMP
29
30 parser = src.options.Options()
31 parser.add_option('n', 'name', 'string', 'name',
32     _('Optional: The name of the application (default is APPLICATION.virtual_app.name or '
33       'runAppli)'))
34 parser.add_option('c', 'catalog', 'string', 'catalog',
35     _('Optional: The resources catalog to use'))
36 parser.add_option('t', 'target', 'string', 'target',
37     _('Optional: The directory where to create the application (default is '
38       'APPLICATION.workdir)'))
39 parser.add_option('', 'gencat', 'string', 'gencat',
40     _("Optional: Create a resources catalog for the specified machines "
41       "(separated with ',')\n\tNOTICE: this command will ssh to retrieve "
42       "information to each machine in the list"))
43 parser.add_option('m', 'module', 'list2', 'modules',
44     _("Optional: the restricted list of module(s) to include in the "
45       "application"))
46 parser.add_option('', 'use_mesa', 'boolean', 'use_mesa',
47     _("Optional: Create a launcher that will use mesa products\n\t"
48       "It can be usefull whan salome is used on a remote machine through ssh"))
49
50 ##
51 # Creates an alias for runAppli.
52 def make_alias(appli_path, alias_path, force=False):
53     assert len(alias_path) > 0, "Bad name for alias"
54     if os.path.exists(alias_path) and not force:
55         raise src.SatException(_("Cannot create the alias '%s'\n") % alias_path)
56     else: # find relative path
57         os.symlink(appli_path, alias_path)
58
59 ##
60 # add the definition of a module to out stream.
61 def add_module_to_appli(out, module, has_gui, module_path, logger, flagline):
62     if not os.path.exists(module_path):
63         if not flagline:
64             logger.write("\n", 3, False)
65             flagline = True
66         logger.write("  " + src.printcolors.printcWarning(_(
67                         "WARNING: module %s not installed") % module) + "\n", 3)
68
69     out.write('   <module name="%s" gui="%s" path="%s"/>\n' % (module,
70                                                                has_gui,
71                                                                module_path))
72     return flagline
73
74 ##
75 # Creates the config file to create an application with the list of modules.
76 def create_config_file(config, modules, env_files, logger):
77
78     samples = ""
79     if 'SAMPLES' in config.APPLICATION.products:
80         samples = src.product.get_product_config(config, 'SAMPLES').source_dir
81
82     config_file = src.get_tmp_filename(config, "appli_config.xml")
83     f = open(config_file, "w")
84
85     f.write('<application>\n')
86     for env_file in env_files:
87         if env_file.endswith("cfg"):
88             f.write('<context path="%s"/>\n' % env_file)
89         else:
90             f.write('<prerequisites path="%s"/>\n' % env_file)
91
92     f.write('<resources path="CatalogResources.xml"/>\n')
93     f.write('<modules>\n')
94
95     flagline = False
96     for m in modules:
97         mm = src.product.get_product_config(config, m)
98         # do not include in virtual application application module!
99         if src.get_property_in_product_cfg(mm, "is_salome_application") == "yes":
100             continue
101         # do not include products that do not compile
102         if not src.product.product_compiles(mm):
103             continue
104         #obsolete?
105         if src.product.product_is_smesh_plugin(mm):
106             continue
107
108         if 'install_dir' in mm and bool(mm.install_dir):
109             if src.product.product_is_cpp(mm):
110                 # cpp module
111                 for aa in src.product.get_product_components(mm):
112                     install_dir=os.path.join(config.APPLICATION.workdir,
113                                              config.INTERNAL.config.install_dir)
114                     mp = os.path.join(install_dir, aa)
115                     flagline = add_module_to_appli(f,
116                                                    aa,
117                                                    "yes",
118                                                    mp,
119                                                    logger,
120                                                    flagline)
121             else:
122                 # regular module
123                 mp = mm.install_dir
124                 gui = src.get_cfg_param(mm, "has_gui", "yes")
125                 flagline = add_module_to_appli(f, m, gui, mp, logger, flagline)
126
127     f.write('</modules>\n')
128     f.write('<samples path="%s"/>\n' % samples)
129     f.write('</application>\n')
130     f.close()
131
132     return config_file
133
134 ##
135 # Customizes the application by editing SalomeApp.xml.
136 def customize_app(config, appli_dir, logger):
137     if 'configure' not in config.APPLICATION.virtual_app \
138         or len(config.APPLICATION.virtual_app.configure) == 0:
139         return
140
141     # shortcut to get an element (section or parameter) from parent.
142     def get_element(parent, name, strtype):
143         for c in parent.getchildren():
144             if c.attrib['name'] == name:
145                 return c
146
147         # element not found create it
148         elt = add_simple_node(parent, strtype)
149         elt.attrib['name'] = name
150         return elt
151
152     # shortcut method to create a node
153     def add_simple_node(parent, node_name, text=None):
154         n = etree.Element(node_name)
155         if text is not None:
156             try:
157                 n.text = text.strip("\n\t").decode("UTF-8")
158             except:
159                 sys.stderr.write("################ %s %s\n" % (node_name, text))
160                 n.text = "?"
161         parent.append(n)
162         return n
163
164     # read the app file
165     app_file = os.path.join(appli_dir, "SalomeApp.xml")
166     tree = etree.parse(app_file)
167     document = tree.getroot()
168     assert document is not None, "document tag not found"
169
170     logger.write("\n", 4)
171     for section_name in config.APPLICATION.virtual_app.configure:
172         for parameter_name in config.APPLICATION.virtual_app.configure[section_name]:
173             parameter_value = config.APPLICATION.virtual_app.configure[section_name][parameter_name]
174             logger.write("  configure: %s/%s = %s\n" % (section_name,
175                                                         parameter_name,
176                                                         parameter_value), 4)
177             section = get_element(document, section_name, "section")
178             parameter = get_element(section, parameter_name, "parameter")
179             parameter.attrib['value'] = parameter_value
180
181     # write the file
182     f = open(app_file, "w")
183     f.write("<?xml version='1.0' encoding='utf-8'?>\n")
184     f.write(etree.tostring(document, encoding='utf-8'))
185     f.close()
186
187 ##
188 # Generates the application with the config_file.
189 def generate_application(config, appli_dir, config_file, logger):
190     target_dir = os.path.dirname(appli_dir)
191
192     install_KERNEL_dir = src.product.get_product_config(config,
193                                                         'KERNEL').install_dir
194     script = os.path.join(install_KERNEL_dir, "bin", "salome", "appli_gen.py")
195     if not os.path.exists(script):
196         raise src.SatException(_("KERNEL is not installed"))
197
198     # Add SALOME python in the environment in order to avoid python version
199     # problems at appli_gen.py call
200     if 'Python' in config.APPLICATION.products:
201         envi = src.environment.SalomeEnviron(config,
202                                              src.environment.Environ(
203                                                               dict(os.environ)),
204                                              True)
205         envi.set_a_product('Python', logger)
206
207     command = "python %s --prefix=%s --config=%s" % (script,
208                                                      appli_dir,
209                                                      config_file)
210     logger.write("\n>" + command + "\n", 5, False)
211     res = subprocess.call(command,
212                     shell=True,
213                     cwd=target_dir,
214                     env=envi.environ.environ,
215                     stdout=logger.logTxtFile,
216                     stderr=subprocess.STDOUT)
217
218     if res != 0:
219         raise src.SatException(_("Cannot create application, code = %d\n") % res)
220
221     return res
222
223 ##
224 #
225 def write_step(logger, message, level=3, pad=50):
226     logger.write("%s %s " % (message, '.' * (pad - len(message.decode("UTF-8")))), level)
227     logger.flush()
228
229 ##
230 # Creates a SALOME application.
231 def create_application(config, appli_dir, catalog, logger, display=True):
232
233     SALOME_modules = get_SALOME_modules(config)
234
235     warn = ['KERNEL', 'GUI']
236     if display:
237         for w in warn:
238             if w not in SALOME_modules:
239                 msg = _("WARNING: module %s is required to create application\n") % w
240                 logger.write(src.printcolors.printcWarning(msg), 2)
241
242     # generate the launch file
243     retcode = generate_launch_file(config,
244                                    appli_dir,
245                                    catalog,
246                                    logger,
247                                    SALOME_modules)
248
249     if retcode == 0:
250         cmd = src.printcolors.printcLabel("%s/salome" % appli_dir)
251
252     if display:
253         logger.write("\n", 3, False)
254         logger.write(_("To launch the application, type:\n"), 3, False)
255         logger.write("  %s" % (cmd), 3, False)
256         logger.write("\n", 3, False)
257     return retcode
258
259 def get_SALOME_modules(config):
260     l_modules = []
261     for product in config.APPLICATION.products:
262         product_info = src.product.get_product_config(config, product)
263         if (src.product.product_is_salome(product_info) or
264                src.product.product_is_generated(product_info)):
265             l_modules.append(product)
266     return l_modules
267
268 ##
269 # Obsolescent way of creating the application.
270 # This method will use appli_gen to create the application directory.
271 def generate_launch_file(config, appli_dir, catalog, logger, l_SALOME_modules):
272     retcode = -1
273
274     if len(catalog) > 0 and not os.path.exists(catalog):
275         raise IOError(_("Catalog not found: %s") % catalog)
276
277     write_step(logger, _("Creating environment files"))
278     status = src.KO_STATUS
279
280     # build the application (the name depends upon salome version
281     env_file = os.path.join(config.APPLICATION.workdir, "env_launch")
282     VersionSalome = src.get_salome_version(config)
283     if VersionSalome>=MMP([8,2,0]):
284         # for salome 8+ we use a salome context file for the virtual app
285         app_shell=["cfg", "bash"]
286         env_files=[env_file+".cfg", env_file+".sh"]
287     else:
288         app_shell=["bash"]
289         env_files=[env_file+".sh"]
290
291     try:
292         import environ
293         # generate only shells the user wants (by default bash, csh, batch)
294         # the environ command will only generate file compatible
295         # with the current system.
296         environ.write_all_source_files(config,
297                                        logger,
298                                        shells=app_shell,
299                                        silent=True)
300         status = src.OK_STATUS
301     finally:
302         logger.write(src.printcolors.printc(status) + "\n", 2, False)
303
304
305     write_step(logger, _("Building application"), level=2)
306     cf = create_config_file(config, l_SALOME_modules, env_files, logger)
307
308     # create the application directory
309     os.makedirs(appli_dir)
310
311     # generate the application
312     status = src.KO_STATUS
313     try:
314         retcode = generate_application(config, appli_dir, cf, logger)
315         customize_app(config, appli_dir, logger)
316         status = src.OK_STATUS
317     finally:
318         logger.write(src.printcolors.printc(status) + "\n", 2, False)
319
320     # copy the catalog if one
321     if len(catalog) > 0:
322         shutil.copy(catalog, os.path.join(appli_dir, "CatalogResources.xml"))
323
324     return retcode
325
326
327
328 ##
329 # Generates the catalog from a list of machines.
330 def generate_catalog(machines, config, logger):
331     # remove empty machines
332     machines = map(lambda l: l.strip(), machines)
333     machines = filter(lambda l: len(l) > 0, machines)
334
335     src.printcolors.print_value(logger,
336                                 _("Generate Resources Catalog"),
337                                 ", ".join(machines),
338                                 4)
339     cmd = '"cat /proc/cpuinfo | grep MHz ; cat /proc/meminfo | grep MemTotal"'
340     user = getpass.getuser()
341
342     catfile = src.get_tmp_filename(config, "CatalogResources.xml")
343     with open(catfile, 'w') as catalog:
344         catalog.write("<!DOCTYPE ResourcesCatalog>\n<resources>\n")
345         for k in machines:
346             if not src.architecture.is_windows():
347                 logger.write("    ssh %s " % (k + " ").ljust(20, '.'), 4)
348                 logger.flush()
349
350                 ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s %s' % (k, cmd)
351                 p = subprocess.Popen(ssh_cmd, shell=True,
352                         stdin=subprocess.PIPE,
353                         stdout=subprocess.PIPE,
354                         stderr=subprocess.PIPE)
355                 p.wait()
356
357                 machine_access = (p.returncode == 0)
358                 if not machine_access:
359                     logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 4)
360                     logger.write("    " + src.printcolors.printcWarning(p.stderr.read()),
361                                  2)
362                 else:
363                     logger.write(src.printcolors.printc(src.OK_STATUS) + "\n", 4)
364                     lines = p.stdout.readlines()
365                     freq = lines[0][:-1].split(':')[-1].split('.')[0].strip()
366                     nb_proc = len(lines) -1
367                     memory = lines[-1].split(':')[-1].split()[0].strip()
368                     memory = int(memory) / 1000
369
370                 catalog.write("    <machine\n")
371                 catalog.write("        protocol=\"ssh\"\n")
372                 catalog.write("        nbOfNodes=\"1\"\n")
373                 catalog.write("        mode=\"interactif\"\n")
374                 catalog.write("        OS=\"LINUX\"\n")
375
376                 if (not src.architecture.is_windows()) and machine_access:
377                     catalog.write("        CPUFreqMHz=\"%s\"\n" % freq)
378                     catalog.write("        nbOfProcPerNode=\"%s\"\n" % nb_proc)
379                     catalog.write("        memInMB=\"%s\"\n" % memory)
380
381                 catalog.write("        userName=\"%s\"\n" % user)
382                 catalog.write("        name=\"%s\"\n" % k)
383                 catalog.write("        hostname=\"%s\"\n" % k)
384                 catalog.write("    >\n")
385                 catalog.write("    </machine>\n")
386
387         catalog.write("</resources>\n")
388     return catfile
389
390 ##################################################
391
392 ##
393 # Describes the command
394 def description():
395     '''method that is called when salomeTools is called with --help option.
396
397     :return: The text to display for the application command description.
398     :rtype: str
399     '''
400     return _("The application command creates a SALOME application.\n"
401              "WARNING: it works only for SALOME 6. Use the \"launcher\" "
402              "command for newer versions of SALOME\n\nexample:\nsat application"
403              " SALOME-6.6.0")
404
405 ##
406 # Runs the command.
407 def run(args, runner, logger):
408     '''method that is called when salomeTools is called with application
409        parameter.
410     '''
411
412     (options, args) = parser.parse_args(args)
413
414     # check for product
415     src.check_config_has_application( runner.cfg )
416
417     application = src.printcolors.printcLabel(runner.cfg.VARS.application)
418     logger.write(_("Building application for %s\n") % application, 1)
419
420     # if section APPLICATION.virtual_app does not exists create one
421     if "virtual_app" not in runner.cfg.APPLICATION:
422         msg = _("The section APPLICATION.virtual_app is not defined in the product. Use sat launcher in state")
423         logger.write(src.printcolors.printcError(msg), 1)
424         logger.write("\n", 1)
425         return 1
426
427     # get application dir
428     target_dir = runner.cfg.APPLICATION.workdir
429     if options.target:
430         target_dir = options.target
431
432     # set list of modules
433     if options.modules:
434         runner.cfg.APPLICATION.virtual_app['modules'] = options.modules
435
436     # activate mesa use in the generated application
437     if options.use_mesa:
438         src.activate_mesa_property(runner.cfg)
439
440     # set name and application_name
441     if options.name:
442         runner.cfg.APPLICATION.virtual_app['name'] = options.name
443         runner.cfg.APPLICATION.virtual_app['application_name'] = options.name + "_appdir"
444
445     application_name = src.get_cfg_param(runner.cfg.APPLICATION.virtual_app,
446                                          "application_name",
447                                          runner.cfg.APPLICATION.virtual_app.name + "_appdir")
448     appli_dir = os.path.join(target_dir, application_name)
449
450     src.printcolors.print_value(logger,
451                                 _("Application directory"),
452                                 appli_dir,
453                                 3)
454
455     # get catalog
456     catalog, catalog_src = "", ""
457     if options.catalog:
458         # use catalog specified in the command line
459         catalog = options.catalog
460     elif options.gencat:
461         # generate catalog for given list of computers
462         catalog_src = options.gencat
463         catalog = generate_catalog(options.gencat.split(","),
464                                    runner.cfg,logger)
465     elif 'catalog' in runner.cfg.APPLICATION.virtual_app:
466         # use catalog specified in the product
467         if runner.cfg.APPLICATION.virtual_app.catalog.endswith(".xml"):
468             # catalog as a file
469             catalog = runner.cfg.APPLICATION.virtual_app.catalog
470         else:
471             # catalog as a list of computers
472             catalog_src = runner.cfg.APPLICATION.virtual_app.catalog
473             mlist = filter(lambda l: len(l.strip()) > 0,
474                            runner.cfg.APPLICATION.virtual_app.catalog.split(","))
475             if len(mlist) > 0:
476                 catalog = generate_catalog(runner.cfg.APPLICATION.virtual_app.catalog.split(","),
477                                            runner.cfg, logger)
478
479     # display which catalog is used
480     if len(catalog) > 0:
481         catalog = os.path.realpath(catalog)
482         if len(catalog_src) > 0:
483             src.printcolors.print_value(logger,
484                                         _("Resources Catalog"),
485                                         catalog_src,
486                                         3)
487         else:
488             src.printcolors.print_value(logger,
489                                         _("Resources Catalog"),
490                                         catalog,
491                                         3)
492
493     logger.write("\n", 3, False)
494
495     details = []
496
497     # remove previous application
498     if os.path.exists(appli_dir):
499         write_step(logger, _("Removing previous application directory"))
500         rres = src.KO_STATUS
501         try:
502             shutil.rmtree(appli_dir)
503             rres = src.OK_STATUS
504         finally:
505             logger.write(src.printcolors.printc(rres) + "\n", 3, False)
506
507     # generate the application
508     try:
509         try: # try/except/finally not supported in all version of python
510             retcode = create_application(runner.cfg, appli_dir, catalog, logger)
511         except Exception as exc:
512             details.append(str(exc))
513             raise
514     finally:
515         logger.write("\n", 3, False)
516
517     return retcode