1#!/usr/bin/python3
2
3#
4# Copyright 2018, The Android Open Source Project
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10#     http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18
19import argparse
20import sys
21import tempfile
22import os
23import logging
24import subprocess
25import xml.etree.ElementTree as ET
26
27import EddParser
28from PFWScriptGenerator import PfwScriptTranslator
29import hostConfig
30
31#
32# In order to build the XML Settings file at build time, an instance of the parameter-framework
33# shall be started and fed with all the criterion types/criteria that will be used by
34# the engineconfigurable.
35# This scripts allows generates the settings from the same audio_criterion_types.xml /
36# audio_criteria.xml files used at run time by the engineconfigurable
37#
38
39def parseArgs():
40    argparser = argparse.ArgumentParser(description="Parameter-Framework XML \
41                                        Settings file generator.\n\
42                                        Exit with the number of (recoverable or not) \
43                                        error that occured.")
44    argparser.add_argument('--domain-generator-tool',
45                           help="ParameterFramework domain generator tool. Mandatory.",
46                           metavar="PFW_DOMAIN_GENERATOR_TOOL",
47                           required=True)
48    argparser.add_argument('--toplevel-config',
49                           help="Top-level parameter-framework configuration file. Mandatory.",
50                           metavar="TOPLEVEL_CONFIG_FILE",
51                           required=True)
52    argparser.add_argument('--criteria',
53                           help="Criteria file, in XML format: \
54                                 in '<criteria> \
55                                         <criterion name="" type=""/> \
56                                     </criteria>' \
57                           format. Mandatory.",
58                           metavar="CRITERIA_FILE",
59                           type=argparse.FileType('r'),
60                           required=True)
61    argparser.add_argument('--criteriontypes',
62                           help="Criterion types XML file, in \
63                           '<criterion_types> \
64                               <criterion_type name="" type=<inclusive|exclusive> \
65                                               values=<value1,value2,...>/> \
66                            </criterion_types>' \
67                           format. Mandatory.",
68                           metavar="CRITERION_TYPE_FILE",
69                           type=argparse.FileType('r'),
70                           required=False)
71    argparser.add_argument('--initial-settings',
72                           help="Initial XML settings file (containing a \
73                           <ConfigurableDomains>  tag",
74                           nargs='?',
75                           default=None,
76                           metavar="XML_SETTINGS_FILE")
77    argparser.add_argument('--add-domains',
78                           help="List of single domain files (each containing a single \
79                           <ConfigurableDomain> tag",
80                           metavar="XML_DOMAIN_FILE",
81                           nargs='*',
82                           dest='xml_domain_files',
83                           default=[])
84    argparser.add_argument('--add-edds',
85                           help="List of files in EDD syntax (aka \".pfw\" files)",
86                           metavar="EDD_FILE",
87                           type=argparse.FileType('r'),
88                           nargs='*',
89                           default=[],
90                           dest='edd_files')
91    argparser.add_argument('--schemas-dir',
92                           help="Directory of parameter-framework XML Schemas for generation \
93                           validation",
94                           default=None)
95    argparser.add_argument('--target-schemas-dir',
96                           help="Ignored. Kept for retro-compatibility")
97    argparser.add_argument('--validate',
98                           help="Validate the settings against XML schemas",
99                           action='store_true')
100    argparser.add_argument('--verbose',
101                           action='store_true')
102
103    return argparser.parse_args()
104
105#
106# Parses audio_criterion_types.xml / audio_criteria.xml files used at run time by the
107# engineconfigurable and outputs a dictionnary of criteria.
108# For each criteria, the name, type (aka inclusive (bitfield) or exclusive (enum), the values
109# are provided.
110#
111def parseCriteriaAndCriterionTypes(criteriaFile, criterionTypesFile):
112    # Parse criteria and criterion types XML files
113    #
114    criteria_tree = ET.parse(criteriaFile)
115    logging.info("Importing criteriaFile {}".format(criteriaFile))
116    criterion_types_tree = ET.parse(criterionTypesFile)
117    logging.info("Importing criterionTypesFile {}".format(criterionTypesFile))
118
119    criteria_root = criteria_tree.getroot()
120
121    all_criteria = []
122    for criterion in criteria_root.findall('criterion'):
123        criterion_name = criterion.get('name')
124        type_name = criterion.get('type')
125        logging.info("Importing criterion_name {}".format(criterion_name))
126        logging.info("Importing type_name {}".format(type_name))
127
128        for criterion_types in criterion_types_tree.findall('criterion_type'):
129            criterion_type_name = criterion_types.get('name')
130            if criterion_type_name == type_name:
131                criterion_inclusiveness = criterion_types.get('type')
132
133                criterion_values = []
134
135                values_node = criterion_types.find('values')
136                if values_node is not None:
137                    for value in values_node.findall('value'):
138                        criterion_values.append(value.get('literal'))
139
140                if len(criterion_values) == 0:
141                    criterion_values.append('')
142
143                logging.info("Importing criterion_type_name {}".format(criterion_type_name))
144                logging.info("Importing criterion_inclusiveness {}".format(criterion_inclusiveness))
145                logging.info("Importing criterion_values {}".format(criterion_values))
146
147                all_criteria.append({
148                    "name" : criterion_name,
149                    "inclusive" : criterion_inclusiveness,
150                    "values" : criterion_values})
151                break
152
153    return all_criteria
154
155#
156# Parses the Edd files (aka .pfw extension file), which is a simplified language to write the
157# parameter framework settings.
158#
159def parseEdd(EDDFiles):
160    parsed_edds = []
161
162    for edd_file in EDDFiles:
163        try:
164            root = EddParser.Parser().parse(edd_file)
165        except EddParser.MySyntaxError as ex:
166            logging.critical(str(ex))
167            logging.info("EXIT ON FAILURE")
168            exit(2)
169
170        try:
171            root.propagate()
172        except EddParser.MyPropagationError as ex:
173            logging.critical(str(ex))
174            logging.info("EXIT ON FAILURE")
175            exit(1)
176
177        parsed_edds.append((edd_file.name, root))
178    return parsed_edds
179
180#
181# Generates all the required commands to be sent to the instance of parameter-framework launched
182# at runtime to generate the XML Settings file.
183# It takes as input the collection of criteria, the domains and the simplified settings read from
184# pfw.
185#
186def generateDomainCommands(logger, all_criteria, initial_settings, xml_domain_files, parsed_edds):
187    # create and inject all the criteria
188    logger.info("Creating all criteria")
189    for criterion in all_criteria:
190        yield ["createSelectionCriterion", criterion['inclusive'],
191               criterion['name']] + criterion['values']
192
193    yield ["start"]
194
195    # Import initial settings file
196    if initial_settings:
197        logger.info("Importing initial settings file {}".format(initial_settings))
198        yield ["importDomainsWithSettingsXML", initial_settings]
199
200    # Import each standalone domain files
201    for domain_file in xml_domain_files:
202        logger.info("Importing single domain file {}".format(domain_file))
203        yield ["importDomainWithSettingsXML", domain_file]
204
205    # Generate the script for each EDD file
206    for filename, parsed_edd in parsed_edds:
207        logger.info("Translating and injecting EDD file {}".format(filename))
208        translator = PfwScriptTranslator()
209        parsed_edd.translate(translator)
210        for command in translator.getScript():
211            yield command
212
213#
214# Entry point of the domain generator.
215#       -Parses Criterion types and criteria files
216#       -Parses settings written in simplified pfw language.
217#       -Launches a parameter-framework
218#       -Translates the settings into command that can be interpreted by parameter-framework.
219#       -Use the exports command and output them in XML Settings file.
220#
221def main():
222    logging.root.setLevel(logging.INFO)
223    args = parseArgs()
224
225    all_criteria = parseCriteriaAndCriterionTypes(args.criteria, args.criteriontypes)
226
227    #
228    # EDD files (aka ".pfw" files)
229    #
230    parsed_edds = parseEdd(args.edd_files)
231
232    # We need to modify the toplevel configuration file to account for differences
233    # between development setup and target (installation) setup, in particular, the
234    # TuningMwith ode must be enforced, regardless of what will be allowed on the target
235    fake_toplevel_config = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".xml",
236                                                       prefix="TMPdomainGeneratorPFConfig_")
237
238    install_path = os.path.dirname(os.path.realpath(args.toplevel_config))
239    hostConfig.configure(infile=args.toplevel_config,
240                         outfile=fake_toplevel_config,
241                         structPath=install_path)
242    fake_toplevel_config.close()
243
244    # Create the connector. Pipe its input to us in order to write commands;
245    # connect its output to stdout in order to have it dump the domains
246    # there; connect its error output to stderr.
247    connector = subprocess.Popen([args.domain_generator_tool,
248                                  fake_toplevel_config.name,
249                                  'verbose' if args.verbose else 'no-verbose',
250                                  'validate' if args.validate else 'no-validate',
251                                  args.schemas_dir],
252                                 stdout=sys.stdout, stdin=subprocess.PIPE, stderr=sys.stderr)
253
254    initial_settings = None
255    if args.initial_settings:
256        initial_settings = os.path.realpath(args.initial_settings)
257
258    for command in generateDomainCommands(logging, all_criteria, initial_settings,
259                                          args.xml_domain_files, parsed_edds):
260        connector.stdin.write('\0'.join(command).encode('utf-8'))
261        connector.stdin.write("\n".encode('utf-8'))
262
263    # Closing the connector's input triggers the domain generation
264    connector.stdin.close()
265    connector.wait()
266    os.remove(fake_toplevel_config.name)
267    return connector.returncode
268
269# If this file is directly executed
270if __name__ == "__main__":
271    sys.exit(main())
272