Salome HOME
add the template command
[tools/sat.git] / commands / template.py
diff --git a/commands/template.py b/commands/template.py
new file mode 100644 (file)
index 0000000..c7cfb32
--- /dev/null
@@ -0,0 +1,538 @@
+#!/usr/bin/env python
+#-*- coding:utf-8 -*-
+#  Copyright (C) 2010-2013  CEA/DEN
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License.
+#
+#  This library is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+
+import os
+import string
+import shutil
+import subprocess
+import fnmatch
+import re
+
+import src
+
+# Compatibility python 2/3 for input function
+# input stays input for python 3 and input = raw_input for python 2
+try: 
+    input = raw_input
+except NameError: 
+    pass
+
+parser = src.options.Options()
+parser.add_option('n', 'name', 'string', 'name',
+    _("""REQUIRED the name of the module to create.
+\tThe name must be a single word in upper case with only alphanumeric characters.
+\tWhen generating a c++ component the module's """
+"""name must be suffixed with 'CPP'."""))
+parser.add_option('t', 'template', 'string', 'template',
+    _('REQUIRED the template to use.'))
+parser.add_option('', 'target', 'string', 'target',
+    _('REQUIRED where to create the module.'))
+parser.add_option('', 'param', 'string', 'param',
+    _('''dictionary to generate the configuration for salomeTools.
+\tFormat is: --param param1=value1,param2=value2... without spaces
+\tNote that when using this option you must supply all the '''
+'''values otherwise an error will be raised.'''))
+parser.add_option('', 'info', 'boolean', 'info',
+    _('Get information on the template.'), False)
+
+class TParam:
+    def __init__(self, param_def, compo_name, dico=None):
+        self.default = ""
+        self.prompt = ""
+        self.check_method = None
+        
+        if isinstance(param_def, str):
+            self.name = param_def
+        elif isinstance(param_def, tuple):
+            self.name = param_def[0]
+            if len(param_def) > 1:
+                if dico is not None: self.default = param_def[1] % dico
+                else: self.default = param_def[1]
+            if len(param_def) > 2: self.prompt = param_def[2]
+            if len(param_def) > 3: self.check_method = param_def[3]
+        else:
+            raise src.SatException(_("ERROR in template parameter definition"))
+
+        self.raw_prompt = self.prompt
+        if len(self.prompt) == 0:
+            self.prompt = _("value for '%s'") % self.name
+        self.prompt += "? "
+        if len(self.default) > 0:
+            self.prompt += "[%s] " % self.default
+
+    def check_value(self, val):
+        if self.check_method is None:
+            return len(val) > 0
+        return len(val) > 0 and self.check_method(val)
+
+def get_dico_param(dico, key, default):
+    if dico.has_key(key):
+        return dico[key]
+    return default
+
+class TemplateSettings:
+    def __init__(self, compo_name, settings_file, target):
+        self.compo_name = compo_name
+        self.dico = None
+        self.target = target
+
+        # read the settings
+        gdic, ldic = {}, {}
+        execfile(settings_file, gdic, ldic)
+
+        # check required parameters in template.info
+        missing = []
+        for pp in ["file_subst", "parameters"]:
+            if not ldic.has_key(pp): missing.append("'%s'" % pp)
+        if len(missing) > 0:
+            raise src.SatException(_(
+                "Bad format in settings file! %s not defined.") % ", ".join(
+                                                                       missing))
+        
+        self.file_subst = ldic["file_subst"]
+        self.parameters = ldic['parameters']
+        self.info = get_dico_param(ldic, "info", "").strip()
+        self.pyconf = get_dico_param(ldic, "pyconf", "")
+        self.post_command = get_dico_param(ldic, "post_command", "")
+
+        # get the delimiter for the template
+        self.delimiter_char = get_dico_param(ldic, "delimiter", ":sat:")
+
+        # get the ignore filter
+        self.ignore_filters = map(lambda l: l.strip(),
+                                  ldic["ignore_filters"].split(','))
+
+    def has_pyconf(self):
+        return len(self.pyconf) > 0
+
+    def get_pyconf_parameters(self):
+        if len(self.pyconf) == 0:
+            return []
+        return re.findall("%\((?P<name>\S[^\)]*)", self.pyconf)
+
+    ##
+    # Check if the file needs to be parsed.
+    def check_file_for_substitution(self, file_):
+        for filter_ in self.ignore_filters:
+            if fnmatch.fnmatchcase(file_, filter_):
+                return False
+        return True
+
+    def check_user_values(self, values):
+        if values is None:
+            return
+        
+        # create a list of all parameters (pyconf + list))
+        pnames = self.get_pyconf_parameters()
+        for p in self.parameters:
+            tp = TParam(p, self.compo_name)
+            pnames.append(tp.name)
+        
+        # reduce the list
+        pnames = list(set(pnames)) # remove duplicates
+
+        known_values = ["name", "Name", "NAME", "target", self.file_subst]
+        known_values.extend(values.keys())
+        missing = []
+        for p in pnames:
+            if p not in known_values:
+                missing.append(p)
+        
+        if len(missing) > 0:
+            raise src.SatException(_(
+                                 "Missing parameters: %s") % ", ".join(missing))
+
+    def get_parameters(self, conf_values=None):
+        if self.dico is not None:
+            return self.dico
+
+        self.check_user_values(conf_values)
+
+        # create dictionary with default values
+        dico = {}
+        dico["name"] = self.compo_name.lower()
+        dico["Name"] = self.compo_name.capitalize()
+        dico["NAME"] = self.compo_name
+        dico["target"] = self.target
+        dico[self.file_subst] = self.compo_name
+        # add user values if any
+        if conf_values is not None:
+            for p in conf_values.keys():
+                dico[p] = conf_values[p]
+
+        # ask user for values
+        for p in self.parameters:
+            tp = TParam(p, self.compo_name, dico)
+            if dico.has_key(tp.name):
+                continue
+            
+            val = ""
+            while not tp.check_value(val):
+                val = raw_input(tp.prompt)
+                if len(val) == 0 and len(tp.default) > 0:
+                    val = tp.default
+            dico[tp.name] = val
+
+        # ask for missing value for pyconf
+        pyconfparam = self.get_pyconf_parameters()
+        for p in filter(lambda l: not dico.has_key(l), pyconfparam):
+            rep = ""
+            while len(rep) == 0:
+                rep = raw_input("%s? " % p)
+            dico[p] = rep
+
+        self.dico = dico
+        return self.dico
+
+def search_template(config, template):
+    # search template
+    template_src_dir = ""
+    if os.path.isabs(template):
+        if os.path.exists(template):
+            template_src_dir = template
+    else:
+        # look in template directory
+        for td in [os.path.join(config.VARS.datadir, "templates")]:
+            zz = os.path.join(td, template)
+            if os.path.exists(zz):
+                template_src_dir = zz
+                break
+
+    if len(template_src_dir) == 0:
+        raise src.SatException(_("Template not found: %s") % template)
+
+    return template_src_dir
+##
+# Prepares a module from a template.
+def prepare_from_template(config,
+                          name,
+                          template,
+                          target_dir,
+                          conf_values,
+                          logger):
+    template_src_dir = search_template(config, template)
+    res = 0
+
+    # copy the template
+    if os.path.isfile(template_src_dir):
+        logger.write("  " + _(
+                        "Extract template %s\n") % src.printcolors.printcInfo(
+                                                                   template), 4)
+        src.system.archive_extract(template_src_dir, target_dir)
+    else:
+        logger.write("  " + _(
+                        "Copy template %s\n") % src.printcolors.printcInfo(
+                                                                   template), 4)
+        shutil.copytree(template_src_dir, target_dir)
+    logger.write("\n", 5)
+
+    compo_name = name
+    if name.endswith("CPP"):
+        compo_name = name[:-3]
+
+    # read settings
+    settings_file = os.path.join(target_dir, "template.info")
+    if not os.path.exists(settings_file):
+        raise src.SatException(_("Settings file not found"))
+    tsettings = TemplateSettings(compo_name, settings_file, target_dir)
+
+    # first rename the files
+    logger.write("  " + src.printcolors.printcLabel(_("Rename files\n")), 4)
+    for root, dirs, files in os.walk(target_dir):
+        for fic in files:
+            ff = fic.replace(tsettings.file_subst, compo_name)
+            if ff != fic:
+                if os.path.exists(os.path.join(root, ff)):
+                    raise src.SatException(_(
+                        "Destination file already exists: %s") % os.path.join(
+                                                                      root, ff))
+                logger.write("    %s -> %s\n" % (fic, ff), 5)
+                os.rename(os.path.join(root, fic), os.path.join(root, ff))
+
+    # rename the directories
+    logger.write("\n", 5)
+    logger.write("  " + src.printcolors.printcLabel(_("Rename directories\n")),
+                 4)
+    for root, dirs, files in os.walk(target_dir, topdown=False):
+        for rep in dirs:
+            dd = rep.replace(tsettings.file_subst, compo_name)
+            if dd != rep:
+                if os.path.exists(os.path.join(root, dd)):
+                    raise src.SatException(_(
+                                "Destination directory "
+                                "already exists: %s") % os.path.join(root, dd))
+                logger.write("    %s -> %s\n" % (rep, dd), 5)
+                os.rename(os.path.join(root, rep), os.path.join(root, dd))
+
+    # ask for missing parameters
+    logger.write("\n", 5)
+    logger.write("  " + src.printcolors.printcLabel(
+                                        _("Make substitution in files\n")), 4)
+    logger.write("    " + _("Delimiter =") + " %s\n" % tsettings.delimiter_char,
+                 5)
+    logger.write("    " + _("Ignore Filters =") + " %s\n" % ', '.join(
+                                                   tsettings.ignore_filters), 5)
+    dico = tsettings.get_parameters(conf_values)
+    logger.write("\n", 3)
+
+    # override standard string.Template class to use the desire delimiter
+    class CompoTemplate(string.Template):
+        delimiter = tsettings.delimiter_char
+
+    # do substitution
+    logger.write("\n", 5, True)
+    pathlen = len(target_dir) + 1
+    for root, dirs, files in os.walk(target_dir):
+        for fic in files:
+            fpath = os.path.join(root, fic)
+            if not tsettings.check_file_for_substitution(fpath[pathlen:]):
+                logger.write("  - %s\n" % fpath[pathlen:], 5)
+                continue
+            # read the file
+            m = file(fpath, 'r').read()
+            # make the substitution
+            template = CompoTemplate(m)
+            d = template.safe_substitute(dico)
+            # overwrite the file with substituted content
+            changed = " "
+            if d != m:
+                changed = "*"
+                file(fpath, 'w').write(d)
+            logger.write("  %s %s\n" % (changed, fpath[pathlen:]), 5)
+
+    if not tsettings.has_pyconf:
+        logger.write(src.printcolors.printcWarning(_(
+                   "Definition for sat not found in settings file.")) + "\n", 2)
+    else:
+        definition = tsettings.pyconf % dico
+        pyconf_file = os.path.join(target_dir, name + '.pyconf')
+        f = open(pyconf_file, 'w')
+        f.write(definition)
+        f.close
+        logger.write(_(
+            "Create configuration file: ") + src.printcolors.printcInfo(
+                                                         pyconf_file) + "\n", 2)
+
+    if len(tsettings.post_command) > 0:
+        cmd = tsettings.post_command % dico
+        logger.write("\n", 5, True)
+        logger.write(_(
+              "Run post command: ") + src.printcolors.printcInfo(cmd) + "\n", 3)
+        
+        p = subprocess.Popen(cmd, shell=True, cwd=target_dir)
+        p.wait()
+        res = p.returncode
+
+    return res
+
+def get_template_info(config, template_name, logger):
+    sources = search_template(config, template_name)
+    src.printcolors.print_value(logger, _("Template"), sources)
+
+    # read settings
+    tmpdir = os.path.join(config.VARS.tmp_root, "tmp_template")
+    settings_file = os.path.join(tmpdir, "template.info")
+    if os.path.exists(tmpdir):
+        shutil.rmtree(tmpdir)
+    if os.path.isdir(sources):
+        shutil.copytree(sources, tmpdir)
+    else:
+        src.system.archive_extract(sources, tmpdir)
+        settings_file = os.path.join(tmpdir, "template.info")
+
+    if not os.path.exists(settings_file):
+        raise src.SatException(_("Settings file not found"))
+    tsettings = TemplateSettings("NAME", settings_file, "target")
+    
+    logger.write("\n", 3)
+    if len(tsettings.info) == 0:
+        logger.write(src.printcolors.printcWarning(_(
+                                       "No information for this template.")), 3)
+    else:
+        logger.write(tsettings.info, 3)
+
+    logger.write("\n", 3)
+    logger.write("= Configuration", 3)
+    src.printcolors.print_value(logger,
+                                "file substitution key",
+                                tsettings.file_subst)
+    src.printcolors.print_value(logger,
+                                "subsitution key",
+                                tsettings.delimiter_char)
+    if len(tsettings.ignore_filters) > 0:
+        src.printcolors.print_value(logger,
+                                    "Ignore Filter",
+                                    ', '.join(tsettings.ignore_filters))
+
+    logger.write("\n", 3)
+    logger.write("= Parameters", 3)
+    pnames = []
+    for pp in tsettings.parameters:
+        tt = TParam(pp, "NAME")
+        pnames.append(tt.name)
+        src.printcolors.print_value(logger, "Name", tt.name)
+        src.printcolors.print_value(logger, "Prompt", tt.raw_prompt)
+        src.printcolors.print_value(logger, "Default value", tt.default)
+        logger.write("\n", 3)
+
+    retcode = 0
+    logger.write("= Verification\n", 3)
+    if tsettings.file_subst not in pnames:
+        logger.write(
+                     "file substitution key not defined as a "
+                     "parameter: %s" % tsettings.file_subst, 3)
+        retcode = 1
+    
+    reexp = tsettings.delimiter_char.replace("$", "\$") + "{(?P<name>\S[^}]*)"
+    pathlen = len(tmpdir) + 1
+    for root, __, files in os.walk(tmpdir):
+        for fic in files:
+            fpath = os.path.join(root, fic)
+            if not tsettings.check_file_for_substitution(fpath[pathlen:]):
+                continue
+            # read the file
+            m = file(fpath, 'r').read()
+            zz = re.findall(reexp, m)
+            zz = list(set(zz)) # reduce
+            zz = filter(lambda l: l not in pnames, zz)
+            if len(zz) > 0:
+                logger.write("Missing definition in %s: %s" % (
+                    src.printcolors.printcLabel(
+                                            fpath[pathlen:]), ", ".join(zz)), 3)
+                retcode = 1
+
+    if retcode == 0:
+        logger.write(src.printcolors.printc("OK"), 3)
+    else:
+        logger.write(src.printcolors.printc("KO"), 3)
+
+    logger.write("\n", 3)
+
+    # clean up tmp file
+    shutil.rmtree(tmpdir)
+
+    return retcode
+
+##
+# Describes the command
+def description():
+    return _("""The template command creates the sources for a SALOME """
+             """module from a template.""")
+
+def run(args, runner, logger):
+    '''method that is called when salomeTools is called with template parameter.
+    '''
+    (options, args) = parser.parse_args(args)
+
+    if options.template is None:
+        msg = _("Error: the --%s argument is required\n") % "template"
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    if options.target is None and options.info is None:
+        msg = _("Error: the --%s argument is required\n") % "target"
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    if "APPLICATION" in runner.cfg:
+        msg = _("Error: this command does not use a product.")
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    if options.info:
+        return get_template_info(runner.cfg, options.template, logger)
+
+    if options.name is None:
+        msg = _("Error: the --%s argument is required\n") % "name"
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    if not options.name.replace('_', '').isalnum():
+        msg = _("Error: component name must contains only alphanumeric "
+                "characters and no spaces\n")
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    # Ask user confirmation if a module of the same name already exists
+    if options.name in runner.cfg.PRODUCTS and not runner.options.batch:
+        logger.write(src.printcolors.printcWarning(
+                    _("A module named '%s' already exists." % options.name)), 1)
+        logger.write("\n", 1)
+        rep = input(_("Are you sure you want to continue? [Yes/No] "))
+        if rep.upper() != _("YES"):
+            return 1
+
+    if options.target is None:
+        msg = _("Error: the --%s argument is required\n") % "target"
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    target_dir = os.path.join(options.target, options.name)
+    if os.path.exists(target_dir):
+        msg = _("Error: the target already exists: %s") % target_dir
+        logger.write(src.printcolors.printcError(msg), 1)
+        logger.write("\n", 1)
+        return 1
+
+    if options.template == "Application":
+        if "_APPLI" not in options.name and not runner.options.batch:
+            msg = _("An Application module named '..._APPLI' "
+                    "is usually recommended.")
+            logger.write(src.printcolors.printcWarning(msg), 1)
+            logger.write("\n", 1)
+            rep = input(_("Are you sure you want to continue? [Yes/No] "))
+            if rep.upper() != _("YES"):
+                return 1
+
+    logger.write(_('Create sources from template\n'), 1)
+    src.printcolors.print_value(logger, 'destination', target_dir, 2)
+    src.printcolors.print_value(logger, 'name', options.name, 2)
+    src.printcolors.print_value(logger, 'template', options.template, 2)
+    logger.write("\n", 3, False)
+    
+    conf_values = None
+    if options.param is not None:
+        conf_values = {}
+        for elt in options.param.split(","):
+            param_def = elt.strip().split('=')
+            if len(param_def) != 2:
+                msg = _("Error: bad parameter definition")
+                logger.write(src.printcolors.printcError(msg), 1)
+                logger.write("\n", 1)
+                return 1
+            conf_values[param_def[0].strip()] = param_def[1].strip()
+    
+    retcode = prepare_from_template(runner.cfg, options.name, options.template,
+        target_dir, conf_values, logger)
+
+    if retcode == 0:
+        logger.write(_(
+                 "The sources were created in %s") % src.printcolors.printcInfo(
+                                                                 target_dir), 3)
+        logger.write(src.printcolors.printcWarning(_("\nDo not forget to put "
+                                   "them in your version control system.")), 3)
+        
+    logger.write("\n", 3)
+    
+    return retcode