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