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