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