1#! /usr/bin/python
2#
3# Copyright (c) 2011-2014, Intel Corporation
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without modification,
7# are permitted provided that the following conditions are met:
8#
9# 1. Redistributions of source code must retain the above copyright notice, this
10# list of conditions and the following disclaimer.
11#
12# 2. Redistributions in binary form must reproduce the above copyright notice,
13# this list of conditions and the following disclaimer in the documentation and/or
14# other materials provided with the distribution.
15#
16# 3. Neither the name of the copyright holder nor the names of its contributors
17# may be used to endorse or promote products derived from this software without
18# specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
24# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
31import PyPfw
32import EddParser
33from PfwBaseTranslator import PfwBaseTranslator, PfwException
34import hostConfig
35
36import argparse
37import re
38import sys
39import tempfile
40import os
41import logging
42
43def wrap_pfw_error_semantic(func):
44    def wrapped(*args, **kwargs):
45        ok, error = func(*args, **kwargs)
46        if not ok:
47            raise PfwException(error)
48
49    return wrapped
50
51class PfwTranslator(PfwBaseTranslator):
52    """Generates calls to the Pfw's python bindings"""
53
54    def __init__(self, pfw_instance, error_handler):
55        super(PfwTranslator, self).__init__()
56        self._pfw = pfw_instance
57        self._error_handler = error_handler
58
59    def _handleException(self, ex):
60        if isinstance(ex, PfwException):
61            # catch and handle translation errors...
62            self._error_handler(ex, self._getContext())
63        else:
64            # ...but let any other error fall through
65            raise ex
66
67    @wrap_pfw_error_semantic
68    def _doCreateDomain(self, name):
69        return self._pfw.createDomain(name)
70
71    @wrap_pfw_error_semantic
72    def _doSetSequenceAware(self):
73        return self._pfw.setSequenceAwareness(self._ctx_domain, True)
74
75    @wrap_pfw_error_semantic
76    def _doAddElement(self, path):
77        return self._pfw.addConfigurableElementToDomain(self._ctx_domain, path)
78
79    @wrap_pfw_error_semantic
80    def _doCreateConfiguration(self, name):
81        return self._pfw.createConfiguration(self._ctx_domain, name)
82
83    @wrap_pfw_error_semantic
84    def _doSetElementSequence(self, paths):
85        return self._pfw.setElementSequence(self._ctx_domain, self._ctx_configuration, paths)
86
87    @wrap_pfw_error_semantic
88    def _doSetRule(self, rule):
89        return self._pfw.setApplicationRule(self._ctx_domain, self._ctx_configuration, rule)
90
91    @wrap_pfw_error_semantic
92    def _doSetParameter(self, path, value):
93        ok, _, error = self._pfw.accessConfigurationValue(
94                self._ctx_domain, self._ctx_configuration, path, value, True)
95
96        return ok, error
97
98
99class PfwTranslationErrorHandler:
100    def __init__(self):
101        self._errors = []
102        self._hasFailed = False
103
104    def __call__(self, error, context):
105        sys.stderr.write("Error in context {}:\n\t{}\n".format(context, error))
106        self._hasFailed = True
107
108    def hasFailed(self):
109        return self._hasFailed
110
111class PfwLogger(PyPfw.ILogger):
112    def __init__(self):
113        super(PfwLogger, self).__init__()
114        self.__logger = logging.root.getChild("parameter-framework")
115
116    def log(self, is_warning, message):
117        log_func = self.__logger.warning if is_warning else self.__logger.info
118        log_func(message)
119
120# If this file is directly executed
121if __name__ == "__main__":
122    logging.root.setLevel(logging.INFO)
123
124    argparser = argparse.ArgumentParser(description="Parameter-Framework XML \
125        Settings file generator")
126    argparser.add_argument('--toplevel-config',
127            help="Top-level parameter-framework configuration file. Mandatory.",
128            metavar="TOPLEVEL_CONFIG_FILE",
129            required=True)
130    argparser.add_argument('--criteria',
131            help="Criteria file, in '<type> <name> : <value> <value...>' \
132        format. Mandatory.",
133            metavar="CRITERIA_FILE",
134            type=argparse.FileType('r'),
135            required=True)
136    argparser.add_argument('--initial-settings',
137            help="Initial XML settings file (containing a \
138        <ConfigurableDomains>  tag",
139            nargs='?',
140            metavar="XML_SETTINGS_FILE")
141    argparser.add_argument('--add-domains',
142            help="List of single domain files (each containing a single \
143        <ConfigurableDomain> tag",
144            metavar="XML_DOMAIN_FILE",
145            nargs='*',
146            dest='xml_domain_files',
147            default=[])
148    argparser.add_argument('--add-edds',
149            help="List of files in EDD syntax (aka \".pfw\" files)",
150            metavar="EDD_FILE",
151            type=argparse.FileType('r'),
152            nargs='*',
153            default=[],
154            dest='edd_files')
155    argparser.add_argument('--schemas-dir',
156            help="Directory of parameter-framework XML Schemas for generation \
157        validation",
158            default=None)
159    argparser.add_argument('--target-schemas-dir',
160            help="Directory of parameter-framework XML Schemas on target \
161        machine (may be different than generating machine). \
162        Defaults to \"Schemas\"",
163            default="Schemas")
164    argparser.add_argument('--validate',
165            help="Validate the settings against XML schemas",
166            action='store_true')
167    argparser.add_argument('--verbose',
168            action='store_true')
169
170    args = argparser.parse_args()
171
172    #
173    # Criteria file
174    #
175    # This file define one criteria per line; they should respect this format:
176    #
177    # <type> <name> : <values>
178    #
179    # Where <type> is 'InclusiveCriterion' or 'ExclusiveCriterion';
180    #       <name> is any string w/o whitespace
181    #       <values> is a list of whitespace-separated values, each of which is any
182    #                string w/o a whitespace
183    criteria_pattern = re.compile(
184        r"^(?P<type>(?:Inclusive|Exclusive)Criterion)\s*" \
185        r"(?P<name>\S+)\s*:\s*" \
186        r"(?P<values>.*)$")
187    criterion_inclusiveness_table = {
188        'InclusiveCriterion' : True,
189        'ExclusiveCriterion' : False}
190    all_criteria = []
191
192    # Parse the criteria file
193    for line_number, line in enumerate(args.criteria, 1):
194        match = criteria_pattern.match(line)
195        if not match:
196            raise ValueError("The following line is invalid: {}:{}\n{}".format(
197                args.criteria.name, line_number, line))
198
199        criterion_name = match.groupdict()['name']
200        criterion_type = match.groupdict()['type']
201        criterion_values = re.split("\s*", match.groupdict()['values'])
202
203        criterion_inclusiveness = criterion_inclusiveness_table[criterion_type]
204
205        all_criteria.append({
206            "name" : criterion_name,
207            "inclusive" : criterion_inclusiveness,
208            "values" : criterion_values})
209
210    #
211    # EDD files (aka ".pfw" files)
212    #
213    parsed_edds = []
214    for edd_file in args.edd_files:
215        try:
216            root = parser = EddParser.Parser().parse(edd_file, args.verbose)
217        except EddParser.MySyntaxError as ex:
218            logging.critical(str(ex))
219            logging.info("EXIT ON FAILURE")
220            exit(2)
221
222        try:
223            root.propagate()
224        except EddParser.MyPropagationError, ex :
225            logging.critical(str(ex))
226            logging.info("EXIT ON FAILURE")
227            exit(1)
228
229        parsed_edds.append((edd_file.name, root))
230
231    # We need to modify the toplevel configuration file to account for differences
232    # between development setup and target (installation) setup, in particular, the
233    # TuningMode must be enforced, regardless of what will be allowed on the target
234    with tempfile.NamedTemporaryFile(mode='w') as fake_toplevel_config:
235        install_path = os.path.dirname(os.path.realpath(args.toplevel_config))
236        hostConfig.configure(
237                infile=args.toplevel_config,
238                outfile=fake_toplevel_config,
239                structPath=install_path)
240        fake_toplevel_config.flush()
241
242        # Create a new Pfw instance
243        pfw = PyPfw.ParameterFramework(fake_toplevel_config.name)
244
245        # create and inject all the criteria
246        logging.info("Creating all criteria")
247        for criterion in all_criteria:
248            criterion_type = pfw.createSelectionCriterionType(criterion['inclusive'])
249
250            for numerical, literal in enumerate(criterion['values']):
251                if criterion['inclusive']:
252                    # inclusive criteria are "bitfields"
253                    numerical = 1 << numerical
254
255                ok = criterion_type.addValuePair(numerical, literal)
256                if not ok:
257                    logging.critical("valuepair {}/{} rejected for {}".format(
258                        numerical, literal, criterion['name']))
259                    exit(1)
260
261            # we don't need the reference to the created criterion type; ignore the
262            # return value
263            pfw.createSelectionCriterion(criterion['name'], criterion_type)
264
265        # Set failure conditions
266        pfw.setFailureOnMissingSubsystem(False)
267        pfw.setFailureOnFailedSettingsLoad(False)
268        if args.validate:
269            pfw.setValidateSchemasOnStart(True)
270            if args.schemas_dir is not None:
271                schemas_dir = args.schemas_dir
272            else:
273                schemas_dir = os.path.join(install_path, "Schemas")
274            pfw.setSchemaFolderLocation(schemas_dir)
275
276        logger = PfwLogger()
277        pfw.setLogger(logger)
278
279        # Disable the remote interface because we don't need it and it might
280        # get in the way (e.g. the port is already in use)
281        pfw.setForceNoRemoteInterface(True)
282
283        # Finally, start the Pfw
284        ok, error = pfw.start()
285        if not ok:
286            logging.critical("Error while starting the pfw: {}".format(error))
287            exit(1)
288
289    ok, error = pfw.setTuningMode(True)
290    if not ok:
291        logging.critical(error)
292        exit(1)
293
294    # Import initial settings file
295    if args.initial_settings:
296        initial_settings = os.path.realpath(args.initial_settings)
297        logging.info(
298            "Importing initial settings file {}".format(initial_settings))
299        ok, error = pfw.importDomainsXml(initial_settings, True, True)
300        if not ok:
301            logging.critical(error)
302            exit(1)
303
304    # Import each standalone domain files
305    for domain_file in args.xml_domain_files:
306        logging.info("Importing single domain file {}".format(domain_file))
307        ok, error = pfw.importSingleDomainXml(os.path.realpath(domain_file),
308                                              False, True, True)
309        if not ok:
310            logging.critical(error)
311            exit(1)
312
313    # Parse and inject each EDD file
314    error_handler = PfwTranslationErrorHandler()
315    translator = PfwTranslator(pfw, error_handler)
316
317    for filename, parsed_edd in parsed_edds:
318        logging.info("Translating and injecting EDD file {}".format(filename))
319        parsed_edd.translate(translator)
320        if error_handler.hasFailed():
321            logging.error("Error while importing parsed EDD files.\n")
322            exit(1)
323
324    # dirty hack: we change the schema location (right before exporting the
325    # domains) to their location on the target (which may be different than on
326    # the machine that is generating the domains)
327    pfw.setSchemaFolderLocation(args.target_schemas_dir)
328
329    # Export the resulting settings to the standard output
330    ok, domains, error = pfw.exportDomainsXml("", True, False)
331    sys.stdout.write(domains)
332