Salome HOME
begin fix sat test
[tools/sat.git] / commands / template.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 string
21 import shutil
22 import subprocess
23 import fnmatch
24 import re
25
26 import src
27
28 # Compatibility python 2/3 for input function
29 # input stays input for python 3 and input = raw_input for python 2
30 try: 
31     input = raw_input
32 except NameError: 
33     pass
34
35 parser = src.options.Options()
36 parser.add_option('n', 'name', 'string', 'name',
37     _("""REQUIRED: the name of the module to create.
38 \tThe name must be a single word in upper case with only alphanumeric characters.
39 \tWhen generating a c++ component the module's """
40 """name must be suffixed with 'CPP'."""))
41 parser.add_option('t', 'template', 'string', 'template',
42     _('REQUIRED: the template to use.'))
43 parser.add_option('', 'target', 'string', 'target',
44     _('REQUIRED: where to create the module.'))
45 parser.add_option('', 'param', 'string', 'param',
46     _('''Optional: dictionary to generate the configuration for salomeTools.
47 \tFormat is: --param param1=value1,param2=value2... without spaces
48 \tNote that when using this option you must supply all the '''
49 '''values otherwise an error will be raised.'''))
50 parser.add_option('', 'info', 'boolean', 'info',
51     _('Optional: Get information on the template.'), False)
52
53 class TParam:
54     def __init__(self, param_def, compo_name, dico=None):
55         self.default = ""
56         self.prompt = ""
57         self.check_method = None
58         
59         if isinstance(param_def, str):
60             self.name = param_def
61         elif isinstance(param_def, tuple):
62             self.name = param_def[0]
63             if len(param_def) > 1:
64                 if dico is not None: self.default = param_def[1] % dico
65                 else: self.default = param_def[1]
66             if len(param_def) > 2: self.prompt = param_def[2]
67             if len(param_def) > 3: self.check_method = param_def[3]
68         else:
69             raise src.SatException(_("ERROR in template parameter definition"))
70
71         self.raw_prompt = self.prompt
72         if len(self.prompt) == 0:
73             self.prompt = _("value for '%s'") % self.name
74         self.prompt += "? "
75         if len(self.default) > 0:
76             self.prompt += "[%s] " % self.default
77
78     def check_value(self, val):
79         if self.check_method is None:
80             return len(val) > 0
81         return len(val) > 0 and self.check_method(val)
82
83 def get_dico_param(dico, key, default):
84     if key in dico:
85         return dico[key]
86     return default
87
88 class TemplateSettings:
89     def __init__(self, compo_name, settings_file, target):
90         self.compo_name = compo_name
91         self.dico = None
92         self.target = target
93
94         # read the settings
95         gdic, ldic = {}, {}
96         execfile(settings_file, gdic, ldic)
97
98         # check required parameters in template.info
99         missing = []
100         for pp in ["file_subst", "parameters"]:
101             if not ldic.has_key(pp): missing.append("'%s'" % pp)
102         if len(missing) > 0:
103             raise src.SatException(_(
104                 "Bad format in settings file! %s not defined.") % ", ".join(
105                                                                        missing))
106         
107         self.file_subst = ldic["file_subst"]
108         self.parameters = ldic['parameters']
109         self.info = get_dico_param(ldic, "info", "").strip()
110         self.pyconf = get_dico_param(ldic, "pyconf", "")
111         self.post_command = get_dico_param(ldic, "post_command", "")
112
113         # get the delimiter for the template
114         self.delimiter_char = get_dico_param(ldic, "delimiter", ":sat:")
115
116         # get the ignore filter
117         self.ignore_filters = map(lambda l: l.strip(),
118                                   ldic["ignore_filters"].split(','))
119
120     def has_pyconf(self):
121         return len(self.pyconf) > 0
122
123     def get_pyconf_parameters(self):
124         if len(self.pyconf) == 0:
125             return []
126         return re.findall("%\((?P<name>\S[^\)]*)", self.pyconf)
127
128     ##
129     # Check if the file needs to be parsed.
130     def check_file_for_substitution(self, file_):
131         for filter_ in self.ignore_filters:
132             if fnmatch.fnmatchcase(file_, filter_):
133                 return False
134         return True
135
136     def check_user_values(self, values):
137         if values is None:
138             return
139         
140         # create a list of all parameters (pyconf + list))
141         pnames = self.get_pyconf_parameters()
142         for p in self.parameters:
143             tp = TParam(p, self.compo_name)
144             pnames.append(tp.name)
145         
146         # reduce the list
147         pnames = list(set(pnames)) # remove duplicates
148
149         known_values = ["name", "Name", "NAME", "target", self.file_subst]
150         known_values.extend(values.keys())
151         missing = []
152         for p in pnames:
153             if p not in known_values:
154                 missing.append(p)
155         
156         if len(missing) > 0:
157             raise src.SatException(_(
158                                  "Missing parameters: %s") % ", ".join(missing))
159
160     def get_parameters(self, conf_values=None):
161         if self.dico is not None:
162             return self.dico
163
164         self.check_user_values(conf_values)
165
166         # create dictionary with default values
167         dico = {}
168         dico["name"] = self.compo_name.lower()
169         dico["Name"] = self.compo_name.capitalize()
170         dico["NAME"] = self.compo_name
171         dico["target"] = self.target
172         dico[self.file_subst] = self.compo_name
173         # add user values if any
174         if conf_values is not None:
175             for p in conf_values.keys():
176                 dico[p] = conf_values[p]
177
178         # ask user for values
179         for p in self.parameters:
180             tp = TParam(p, self.compo_name, dico)
181             if tp.name in dico:
182                 continue
183             
184             val = ""
185             while not tp.check_value(val):
186                 val = raw_input(tp.prompt)
187                 if len(val) == 0 and len(tp.default) > 0:
188                     val = tp.default
189             dico[tp.name] = val
190
191         # ask for missing value for pyconf
192         pyconfparam = self.get_pyconf_parameters()
193         for p in filter(lambda l: not dico.has_key(l), pyconfparam):
194             rep = ""
195             while len(rep) == 0:
196                 rep = raw_input("%s? " % p)
197             dico[p] = rep
198
199         self.dico = dico
200         return self.dico
201
202 def search_template(config, template):
203     # search template
204     template_src_dir = ""
205     if os.path.isabs(template):
206         if os.path.exists(template):
207             template_src_dir = template
208     else:
209         # look in template directory
210         for td in [os.path.join(config.VARS.datadir, "templates")]:
211             zz = os.path.join(td, template)
212             if os.path.exists(zz):
213                 template_src_dir = zz
214                 break
215
216     if len(template_src_dir) == 0:
217         raise src.SatException(_("Template not found: %s") % template)
218
219     return template_src_dir
220 ##
221 # Prepares a module from a template.
222 def prepare_from_template(config,
223                           name,
224                           template,
225                           target_dir,
226                           conf_values,
227                           logger):
228     template_src_dir = search_template(config, template)
229     res = 0
230
231     # copy the template
232     if os.path.isfile(template_src_dir):
233         logger.write("  " + _(
234                         "Extract template %s\n") % src.printcolors.printcInfo(
235                                                                    template), 4)
236         src.system.archive_extract(template_src_dir, target_dir)
237     else:
238         logger.write("  " + _(
239                         "Copy template %s\n") % src.printcolors.printcInfo(
240                                                                    template), 4)
241         shutil.copytree(template_src_dir, target_dir)
242     logger.write("\n", 5)
243
244     compo_name = name
245     if name.endswith("CPP"):
246         compo_name = name[:-3]
247
248     # read settings
249     settings_file = os.path.join(target_dir, "template.info")
250     if not os.path.exists(settings_file):
251         raise src.SatException(_("Settings file not found"))
252     tsettings = TemplateSettings(compo_name, settings_file, target_dir)
253
254     # first rename the files
255     logger.write("  " + src.printcolors.printcLabel(_("Rename files\n")), 4)
256     for root, dirs, files in os.walk(target_dir):
257         for fic in files:
258             ff = fic.replace(tsettings.file_subst, compo_name)
259             if ff != fic:
260                 if os.path.exists(os.path.join(root, ff)):
261                     raise src.SatException(_(
262                         "Destination file already exists: %s") % os.path.join(
263                                                                       root, ff))
264                 logger.write("    %s -> %s\n" % (fic, ff), 5)
265                 os.rename(os.path.join(root, fic), os.path.join(root, ff))
266
267     # rename the directories
268     logger.write("\n", 5)
269     logger.write("  " + src.printcolors.printcLabel(_("Rename directories\n")),
270                  4)
271     for root, dirs, files in os.walk(target_dir, topdown=False):
272         for rep in dirs:
273             dd = rep.replace(tsettings.file_subst, compo_name)
274             if dd != rep:
275                 if os.path.exists(os.path.join(root, dd)):
276                     raise src.SatException(_(
277                                 "Destination directory "
278                                 "already exists: %s") % os.path.join(root, dd))
279                 logger.write("    %s -> %s\n" % (rep, dd), 5)
280                 os.rename(os.path.join(root, rep), os.path.join(root, dd))
281
282     # ask for missing parameters
283     logger.write("\n", 5)
284     logger.write("  " + src.printcolors.printcLabel(
285                                         _("Make substitution in files\n")), 4)
286     logger.write("    " + _("Delimiter =") + " %s\n" % tsettings.delimiter_char,
287                  5)
288     logger.write("    " + _("Ignore Filters =") + " %s\n" % ', '.join(
289                                                    tsettings.ignore_filters), 5)
290     dico = tsettings.get_parameters(conf_values)
291     logger.write("\n", 3)
292
293     # override standard string.Template class to use the desire delimiter
294     class CompoTemplate(string.Template):
295         delimiter = tsettings.delimiter_char
296
297     # do substitution
298     logger.write("\n", 5, True)
299     pathlen = len(target_dir) + 1
300     for root, dirs, files in os.walk(target_dir):
301         for fic in files:
302             fpath = os.path.join(root, fic)
303             if not tsettings.check_file_for_substitution(fpath[pathlen:]):
304                 logger.write("  - %s\n" % fpath[pathlen:], 5)
305                 continue
306             # read the file
307             m = file(fpath, 'r').read()
308             # make the substitution
309             template = CompoTemplate(m)
310             d = template.safe_substitute(dico)
311             # overwrite the file with substituted content
312             changed = " "
313             if d != m:
314                 changed = "*"
315                 file(fpath, 'w').write(d)
316             logger.write("  %s %s\n" % (changed, fpath[pathlen:]), 5)
317
318     if not tsettings.has_pyconf:
319         logger.write(src.printcolors.printcWarning(_(
320                    "Definition for sat not found in settings file.")) + "\n", 2)
321     else:
322         definition = tsettings.pyconf % dico
323         pyconf_file = os.path.join(target_dir, name + '.pyconf')
324         f = open(pyconf_file, 'w')
325         f.write(definition)
326         f.close
327         logger.write(_(
328             "Create configuration file: ") + src.printcolors.printcInfo(
329                                                          pyconf_file) + "\n", 2)
330
331     if len(tsettings.post_command) > 0:
332         cmd = tsettings.post_command % dico
333         logger.write("\n", 5, True)
334         logger.write(_(
335               "Run post command: ") + src.printcolors.printcInfo(cmd) + "\n", 3)
336         
337         p = subprocess.Popen(cmd, shell=True, cwd=target_dir)
338         p.wait()
339         res = p.returncode
340
341     return res
342
343 def get_template_info(config, template_name, logger):
344     sources = search_template(config, template_name)
345     src.printcolors.print_value(logger, _("Template"), sources)
346
347     # read settings
348     tmpdir = os.path.join(config.VARS.tmp_root, "tmp_template")
349     settings_file = os.path.join(tmpdir, "template.info")
350     if os.path.exists(tmpdir):
351         shutil.rmtree(tmpdir)
352     if os.path.isdir(sources):
353         shutil.copytree(sources, tmpdir)
354     else:
355         src.system.archive_extract(sources, tmpdir)
356         settings_file = os.path.join(tmpdir, "template.info")
357
358     if not os.path.exists(settings_file):
359         raise src.SatException(_("Settings file not found"))
360     tsettings = TemplateSettings("NAME", settings_file, "target")
361     
362     logger.write("\n", 3)
363     if len(tsettings.info) == 0:
364         logger.write(src.printcolors.printcWarning(_(
365                                        "No information for this template.")), 3)
366     else:
367         logger.write(tsettings.info, 3)
368
369     logger.write("\n", 3)
370     logger.write("= Configuration", 3)
371     src.printcolors.print_value(logger,
372                                 "file substitution key",
373                                 tsettings.file_subst)
374     src.printcolors.print_value(logger,
375                                 "subsitution key",
376                                 tsettings.delimiter_char)
377     if len(tsettings.ignore_filters) > 0:
378         src.printcolors.print_value(logger,
379                                     "Ignore Filter",
380                                     ', '.join(tsettings.ignore_filters))
381
382     logger.write("\n", 3)
383     logger.write("= Parameters", 3)
384     pnames = []
385     for pp in tsettings.parameters:
386         tt = TParam(pp, "NAME")
387         pnames.append(tt.name)
388         src.printcolors.print_value(logger, "Name", tt.name)
389         src.printcolors.print_value(logger, "Prompt", tt.raw_prompt)
390         src.printcolors.print_value(logger, "Default value", tt.default)
391         logger.write("\n", 3)
392
393     retcode = 0
394     logger.write("= Verification\n", 3)
395     if tsettings.file_subst not in pnames:
396         logger.write(
397                      "file substitution key not defined as a "
398                      "parameter: %s" % tsettings.file_subst, 3)
399         retcode = 1
400     
401     reexp = tsettings.delimiter_char.replace("$", "\$") + "{(?P<name>\S[^}]*)"
402     pathlen = len(tmpdir) + 1
403     for root, __, files in os.walk(tmpdir):
404         for fic in files:
405             fpath = os.path.join(root, fic)
406             if not tsettings.check_file_for_substitution(fpath[pathlen:]):
407                 continue
408             # read the file
409             m = file(fpath, 'r').read()
410             zz = re.findall(reexp, m)
411             zz = list(set(zz)) # reduce
412             zz = filter(lambda l: l not in pnames, zz)
413             if len(zz) > 0:
414                 logger.write("Missing definition in %s: %s" % (
415                     src.printcolors.printcLabel(
416                                             fpath[pathlen:]), ", ".join(zz)), 3)
417                 retcode = 1
418
419     if retcode == 0:
420         logger.write(src.printcolors.printc("OK"), 3)
421     else:
422         logger.write(src.printcolors.printc("KO"), 3)
423
424     logger.write("\n", 3)
425
426     # clean up tmp file
427     shutil.rmtree(tmpdir)
428
429     return retcode
430
431 ##
432 # Describes the command
433 def description():
434     return _("The template command creates the sources for a SALOME "
435              "module from a template.\n\nexample\nsat template "
436              "--name my_product_name --template PythonComponent --target /tmp")
437
438 def run(args, runner, logger):
439     '''method that is called when salomeTools is called with template parameter.
440     '''
441     (options, args) = parser.parse_args(args)
442
443     if options.template is None:
444         msg = _("Error: the --%s argument is required\n") % "template"
445         logger.write(src.printcolors.printcError(msg), 1)
446         logger.write("\n", 1)
447         return 1
448
449     if options.target is None and options.info is None:
450         msg = _("Error: the --%s argument is required\n") % "target"
451         logger.write(src.printcolors.printcError(msg), 1)
452         logger.write("\n", 1)
453         return 1
454
455     if "APPLICATION" in runner.cfg:
456         msg = _("Error: this command does not use a product.")
457         logger.write(src.printcolors.printcError(msg), 1)
458         logger.write("\n", 1)
459         return 1
460
461     if options.info:
462         return get_template_info(runner.cfg, options.template, logger)
463
464     if options.name is None:
465         msg = _("Error: the --%s argument is required\n") % "name"
466         logger.write(src.printcolors.printcError(msg), 1)
467         logger.write("\n", 1)
468         return 1
469
470     if not options.name.replace('_', '').isalnum():
471         msg = _("Error: component name must contains only alphanumeric "
472                 "characters and no spaces\n")
473         logger.write(src.printcolors.printcError(msg), 1)
474         logger.write("\n", 1)
475         return 1
476
477     # CNC inutile
478     # Ask user confirmation if a module of the same name already exists
479     #if options.name in runner.cfg.PRODUCTS and not runner.options.batch:
480     #    logger.write(src.printcolors.printcWarning(
481     #                _("A module named '%s' already exists." % options.name)), 1)
482     #    logger.write("\n", 1)
483     #    rep = input(_("Are you sure you want to continue? [Yes/No] "))
484     #    if rep.upper() != _("YES"):
485     #        return 1
486
487     if options.target is None:
488         msg = _("Error: the --%s argument is required\n") % "target"
489         logger.write(src.printcolors.printcError(msg), 1)
490         logger.write("\n", 1)
491         return 1
492
493     target_dir = os.path.join(options.target, options.name)
494     if os.path.exists(target_dir):
495         msg = _("Error: the target already exists: %s") % target_dir
496         logger.write(src.printcolors.printcError(msg), 1)
497         logger.write("\n", 1)
498         return 1
499
500     # CNC inutile
501     #if options.template == "Application":
502     #    if "_APPLI" not in options.name and not runner.options.batch:
503     #        msg = _("An Application module named '..._APPLI' "
504     #                "is usually recommended.")
505     #        logger.write(src.printcolors.printcWarning(msg), 1)
506     #        logger.write("\n", 1)
507     #        rep = input(_("Are you sure you want to continue? [Yes/No] "))
508     #        if rep.upper() != _("YES"):
509     #            return 1
510
511     logger.write(_('Create sources from template\n'), 1)
512     src.printcolors.print_value(logger, 'destination', target_dir, 2)
513     src.printcolors.print_value(logger, 'name', options.name, 2)
514     src.printcolors.print_value(logger, 'template', options.template, 2)
515     logger.write("\n", 3, False)
516     
517     conf_values = None
518     if options.param is not None:
519         conf_values = {}
520         for elt in options.param.split(","):
521             param_def = elt.strip().split('=')
522             if len(param_def) != 2:
523                 msg = _("Error: bad parameter definition")
524                 logger.write(src.printcolors.printcError(msg), 1)
525                 logger.write("\n", 1)
526                 return 1
527             conf_values[param_def[0].strip()] = param_def[1].strip()
528     
529     retcode = prepare_from_template(runner.cfg, options.name, options.template,
530         target_dir, conf_values, logger)
531
532     if retcode == 0:
533         logger.write(_(
534                  "The sources were created in %s") % src.printcolors.printcInfo(
535                                                                  target_dir), 3)
536         logger.write(src.printcolors.printcWarning(_("\nDo not forget to put "
537                                    "them in your version control system.")), 3)
538         
539     logger.write("\n", 3)
540     
541     return retcode