1###############################################################################
2# Copyright (c) 2013 INRIA
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License version 2 as
6# published by the Free Software Foundation;
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16#
17# Authors: Daniel Camara  <daniel.camara@inria.fr>
18#          Mathieu Lacage <mathieu.lacage@sophia.inria.fr>
19###############################################################################
20'''
21 Configuration.py
22
23 The main purpose of this file is to store all the classes related
24 to the configuration of Bake.
25'''
26
27import os
28import re
29import sys
30import xml.etree.ElementTree as ET
31try:
32 from xml.etree.ElementTree import ParseError
33except ImportError:
34 from xml.parsers.expat import ExpatError as ParseError
35from bake.Module import Module, ModuleDependency
36from bake.ModuleSource import ModuleSource, InlineModuleSource
37from bake.ModuleBuild import ModuleBuild, InlineModuleBuild
38from bake.Exceptions import MetadataError
39from bake.Exceptions import TaskError
40
41class MetadataFile:
42    """Stores the meta information of a given file."""
43
44    def __init__(self, filename, h=''):
45        self._filename = os.path.realpath(filename)
46        self._h = h
47
48    def filename(self):
49        return self._filename
50
51    def h(self):
52        import hashlib
53        m = hashlib.sha256()
54        try:
55            f = open(self._filename)
56            m.update(f.read())
57            f.close()
58            return m.hexdigest()
59        except:
60            return ''
61
62    def is_hash_ok(self):
63        """Verifies if the hash of the configuration file is OK, to avoid
64        manual and transmission changes.
65        """
66
67        return self.h() == self._h
68
69class PredefinedConfiguration:
70    """Stores the information of predefined options."""
71
72    def __init__(self, name, enable, disable, variables_set, variables_append,
73                 directories):
74        self.name = name
75        self.enable = enable
76        self.disable = disable
77        self.variables_set = variables_set
78        self.variables_append = variables_append
79        self.directories = directories
80
81class Configuration:
82    """Main configuration class."""
83
84    def __init__(self, bakefile, relative_directory_root=None):
85        self._enabled = []
86        self._disabled = []
87        self._modules = []
88        self._configured = []
89        self._installdir = None
90        self._objdir = None
91        self._sourcedir = None
92        self._metadata_file = None
93#        self._bakefile = os.path.abspath(bakefile)
94        if bakefile.startswith(os.sep):
95            self._bakefile = os.path.abspath(bakefile)
96        else:
97            self._bakefile = os.getcwd()+os.sep+bakefile
98        if relative_directory_root is None:
99            self._relative_directory_root = os.path.relpath(os.getcwd(),
100                                                            os.path.dirname(self._bakefile))
101        else:
102            self._relative_directory_root = relative_directory_root
103
104    def read_metadata(self, filename):
105        """ Reads the list of meta-data defined in the XML config file"""
106
107        if not os.path.exists(filename):
108            self._error('Could not find "%s"' % filename)
109
110        self._metadata_file = MetadataFile(filename)
111        et = ET.parse(filename)
112        self._read_metadata(et)
113
114    def read_predefined(self, filename):
115        """ Creates the list of predefined entries defined in the XML
116        configuration file
117        """
118        predefined = []
119
120        try:
121            et = ET.parse(filename)
122        except ParseError:
123            return predefined
124
125        for pred_node in et.getroot().findall('predefined'):
126            name = pred_node.get('name', None)
127
128            if not name:
129                self._error('<predefined> must define a "name" attribute.')
130
131            enable = []
132            for enable_node in pred_node.findall('enable'):
133                enable_name = enable_node.get('name', None)
134
135                if not enable_name:
136                    self._error('<enable> must define a "name" attribute.')
137                enable.append(enable_name)
138
139            disable = []
140            for disable_node in pred_node.findall('disable'):
141                disable_name = disable_node.get('name', None)
142
143                if not disable_name:
144                    self._error('<disable> must define a "name" attribute.')
145                disable.append(disable_name)
146
147            variables_set = []
148            for set_node in pred_node.findall('set'):
149                set_name = set_node.get('name', None)
150                set_value = set_node.get('value', None)
151                set_module = set_node.get('module', None)
152
153                if not set_name or not set_value:
154                    self._error('<set> must define a "name" and a "value" attribute.')
155                variables_set.append((set_module, set_name, set_value))
156
157            variables_append = []
158            for append_node in pred_node.findall('append'):
159                append_name = append_node.get('name', None)
160                append_value = append_node.get('value', None)
161                append_module = append_node.get('module', None)
162
163                if not append_name or not append_value:
164                    self._error('<append> must define a "name" and a "value" attribute.')
165                variables_append.append((append_module, append_name, append_value))
166
167            directories = {}
168            for config_node in pred_node.findall('configuration'):
169                objdir = config_node.get('objdir', None)
170                installdir = config_node.get('installdir', None)
171                sourcedir = config_node.get('sourcedir', None)
172
173                if objdir:
174                    directories['objdir'] = objdir
175
176                if installdir:
177                    directories['installdir'] = installdir
178
179                if sourcedir:
180                    directories['sourcedir'] = sourcedir
181
182            predefined.append(PredefinedConfiguration(name, enable, disable,
183                                                      variables_set,
184                                                      variables_append,
185                                                      directories))
186        return predefined
187
188    def _error(self, string):
189        """ Handles the exceptions """
190        raise Exception(string)
191
192    def _check_mandatory_attributes(self, attribute_base, node, type_string,
193                                    module_string):
194        """ Checks the existence of the mandatory attributes for each
195        configuration.
196        """
197
198        # get list of names in <attribute name="" value=""> tags
199        attributes_present = [child.get('name') for child in node.findall('attribute')]
200        # get list of names in <type_string name="value"> attributes
201        attributes_present = attributes_present + list(node.attrib)
202
203        for attribute in attribute_base.attributes():
204            if attribute.is_mandatory and not attribute.name in attributes_present:
205                sys.stderr.write('Error: mandatory attribute "%s" is missing from '
206                                 'module "%s" in node "%s"\n' % (attribute.name,
207                                                                 module_string,
208                                                                 type_string))
209                sys.exit(1)
210
211    def _read_attributes(self, obj, node, type_string, module_string):
212        """ Reads the list of attributes on the configuration configuration."""
213
214        # read <type_string><attribute name="" value=""></type_string> tags
215        for attribute_node in node.findall('attribute'):
216            attr_name = attribute_node.get('name')
217            attr_value = attribute_node.get('value', None)
218            if obj.attribute(attr_name) is None:
219                sys.stderr.write('Error: attribute "%s" is not supported by'
220                                 ' %s node of type "%s"\n' %
221                                 (attr_name, type_string, node.get('type')))
222                sys.exit(1)
223            obj.attribute(attr_name).value = attr_value
224
225        # as a fallback, read <type_string name="value"> attributes
226        # note: this will not generate errors upon invalid attribute names
227        # because certain kinds of <foo name="value"/> XML attributes are
228        # not handled as bake attributes.
229        for attr_name in node.attrib.keys():
230            if not obj.attribute(attr_name) is None:
231                obj.attribute(attr_name).value = node.get(attr_name)
232
233    def _write_attributes(self, attribute_base, obj_node):
234        """ Creates the XML elements, reflecting the listed attributes."""
235
236        # generate <attribute name="" value=""/> tags
237        for attribute in attribute_base.attributes():
238            if not attribute.value is None:
239                attribute_node = ET.Element('attribute', {'name' : attribute.name,
240                                                          'value' : attribute.value})
241                obj_node.append(attribute_node)
242
243    def _create_obj_from_node(self, node, classBase, node_string, module_name):
244        """ Translates the XML elements on the correct bake object."""
245
246        # read <node_string type=""> tag: handle type="inline" specially by
247        # looking up a child node <code></code>
248        if node.get('type') == 'inline':
249            code_node = node.find('code')
250            if code_node is None:
251                sys.stderr.write('Error: no code tag in inline module\n')
252                sys.exit(1)
253
254            classname = node.get('classname')
255            import codeop
256            exec(code_node.text, globals(), locals())
257            obj = eval(classname + '()')
258            obj.__hidden_source_code = code_node.text
259        else:
260            obj = classBase.create(node.get('type'))
261
262        self._check_mandatory_attributes(obj, node, node_string, module_name)
263        self._read_attributes(obj, node, node_string, module_name)
264
265        # if <type_string> has <child> nodes, look them up.
266        for child_node in node.findall('child'):
267            child_name = child_node.get('name')
268            child = self._create_obj_from_node(child_node, classBase, 'child',
269                                               module_name)
270            obj.add_child(child, child_name)
271
272        return obj
273
274    def _create_node_from_obj(self, obj, node_string):
275        """ Generates the XML node based on the XML object passed as parameter"""
276
277        # inline is when one uses Python as build configuration to create a
278        # small build script
279        if obj.__class__.name() == 'inline':
280            node = ET.Element(node_string, {'type' : 'inline',
281                                            'classname' : obj.__class__.__name__})
282            code = ET.Element('code')
283            code.text = obj.__hidden_source_code
284            node.append(code)
285        else:
286            node = ET.Element(node_string, {'type' : obj.__class__.name()})
287
288        self._write_attributes(obj, node)
289
290        for child, child_name in obj.children():
291            child_node = self._create_node_from_obj(child, 'child')
292            child_node.attrib['name'] = child_name
293            node.append(child_node)
294
295        return node
296
297    def _read_installed(self, node):
298        """ Reads the installed modules from the XML."""
299
300        installed = []
301        for installed_node in node.findall('installed'):
302            installed.append(installed_node.get('value', None))
303        return installed
304
305    def _write_installed(self, node, installed):
306        """ Generates the XML nodes to register the installed modules."""
307
308        for installed in installed:
309            installed_node = ET.Element('installed', {'value' : installed})
310            node.append(installed_node)
311
312
313    def _read_metadata(self, et):
314        """ Reads the elements from the xml configuration files and add it to
315        the internal list of modules.
316        """
317
318        # function designed to be called on two kinds of xml files.
319        modules = et.findall('modules/module')
320        for module_node in modules:
321            name = module_node.get('name')
322            mtype = module_node.get('type')
323            min_ver = module_node.get('min_version')
324            max_ver = module_node.get('max_version')
325            installed = self._read_installed(module_node)
326
327            source_node = module_node.find('source')
328            source = self._create_obj_from_node(source_node, ModuleSource,
329                                                'source', name)
330
331            build_node = module_node.find('build')
332            build = self._create_obj_from_node(build_node, ModuleBuild,
333                                               'build', name)
334#            self._read_libpath(build_node, build)
335
336            dependencies = []
337            for dep_node in module_node.findall('depends_on'):
338                dependencies.append(self._create_obj_from_node(dep_node,ModuleDependency,'depends_on',name))
339
340            module = Module(name, source, build, mtype, min_ver, max_ver, dependencies=dependencies,
341                            built_once=bool(module_node.get('built_once', '').upper()=='TRUE'),
342                            installed=installed)
343            self._modules.append(module)
344
345    def _write_metadata(self, root):
346        """ Saves modules data to the XML configuration file."""
347
348        modules_node = ET.Element('modules')
349        root.append(modules_node)
350
351        for module in self._modules:
352            module_attrs = {'name' : module.name()}
353            if module.mtype():
354                module_attrs['type'] = module.mtype()
355            if module.minver():
356                module_attrs['min_version'] = module.minver()
357            if module.is_built_once():
358                module_attrs['built_once'] = 'True'
359            module_node = ET.Element('module', module_attrs)
360            self._write_installed(module_node, module.installed)
361
362            # registers the values, possible changed ones, from the source and
363            # build XML tags of each module
364            source_node = self._create_node_from_obj(module.get_source(),
365                                                     'source')
366            module_node.append(source_node)
367
368            build_node = self._create_node_from_obj(module.get_build(), 'build')
369            module_node.append(build_node)
370#            self._write_libpath(build_node, module.get_build())
371
372            # handles the dependencies for the module and register them
373            # into module node
374            for dependency in module.dependencies():
375                dep_node = self._create_node_from_obj(dependency, 'depends_on')
376                module_node.append(dep_node)
377            modules_node.append(module_node)
378
379    def defineXml(self):
380        """ Creates the basic XML structure for the configuration file."""
381
382        root = ET.Element('configuration', {'installdir':self._installdir,
383                'sourcedir':self._sourcedir,
384                'objdir':self._objdir,
385                'relative_directory_root':self._relative_directory_root,
386                'bakefile':self._bakefile})
387
388        if not self._metadata_file is None:
389            metadata = ET.Element('metadata',
390                                  {'filename':self._metadata_file.filename(),
391                                   'hash':self._metadata_file.h()})
392            root.append(metadata)
393
394        # write enabled nodes
395        for e in self._enabled:
396            enable_node = ET.Element('enabled', {'name':e.name()})
397            root.append(enable_node)
398
399        # write disabled nodes
400        for e in self._disabled:
401            disable_node = ET.Element('disabled', {'name':e.name()})
402            root.append(disable_node)
403
404        # add modules information
405        self._write_metadata(root)
406        et = ET.ElementTree(element=root)
407        return et
408
409    def write(self):
410        """ Creates the target configuration XML file."""
411
412        et = self.defineXml()
413
414        try:
415            et.write(self._bakefile)
416        except IOError as e:
417            raise TaskError('Problems writing the file, error: %s' % e)
418
419    def read(self):
420        """ Reads the XML customized configuration file."""
421
422        try:
423            et = ET.parse(self._bakefile)
424        except IOError as e:
425            err = re.sub(r'\[\w+ \w+\]+', ' ', str(e)).strip()
426            raise TaskError('>> Problems reading the configuration file, verify if'
427                            ' it exists or try calling bake.py configure. \n'
428                            '   Error: %s' % err)
429
430        self._read_metadata(et)
431        root = et.getroot()
432        self._installdir = root.get('installdir')
433        self._objdir = root.get('objdir')
434        self._sourcedir = root.get('sourcedir')
435        self._relative_directory_root = root.get('relative_directory_root')
436        original_bakefile = root.get('bakefile')
437        metadata = root.find('metadata')
438
439        if metadata is not None:
440            self._metadata_file = MetadataFile (metadata.get('filename'),
441                                            h=metadata.get('hash'))
442
443        # read which modules are enabled
444        modules = root.findall('enabled')
445        for module in modules:
446            self._configured.append(self.lookup(module.get('name')))
447            enabled = self.lookup(module.get('name'))
448            self.enable(enabled)
449
450        # read which modules are disabled
451        modules = root.findall('disabled')
452        for module in modules:
453            disabled = self.lookup(module.get('name'))
454            self.disable(disabled)
455
456        if metadata  is not None:
457            return self._metadata_file.is_hash_ok() #and original_bakefile == self._bakefile
458        else :
459            return True
460
461    def set_installdir(self, installdir):
462        self._installdir = installdir
463
464    def get_installdir(self):
465        return self._installdir
466
467    def set_objdir(self, objdir):
468        self._objdir = objdir
469
470    def get_objdir(self):
471        return self._objdir
472
473    def set_sourcedir(self, sourcedir):
474        self._sourcedir = sourcedir
475
476    def get_sourcedir(self):
477        return self._sourcedir
478
479    def get_relative_directory_root(self):
480        return self._relative_directory_root
481
482    def _compute_path(self, p):
483        """Returns the full path"""
484
485        if os.path.isabs(p):
486            return p
487        else:
488            tmp = os.path.join(os.path.dirname(self._bakefile),
489                               self._relative_directory_root, p)
490            return os.path.normpath(tmp)
491
492    def compute_sourcedir(self):
493        return self._compute_path(self._sourcedir)
494
495    def compute_installdir(self):
496        return self._compute_path(self._installdir)
497
498    def enable(self, module):
499        """ Set the module as enabled, but if it is disabled, simply removes
500        it from the disabled list.
501        """
502
503        if module in self._disabled:
504            self._disabled.remove(module)
505        elif module not in self._enabled:
506            self._enabled.append(module)
507
508    def disable(self, module):
509        """ Set the module as disabled, but if it is enabled, simply removes
510        it from the enabled list.
511        """
512
513        if module in self._enabled:
514            self._enabled.remove(module)
515        else:
516            self._disabled.append(module)
517
518    def lookup(self, name):
519        """ Finds the module in the modules list."""
520
521        for module in self._modules:
522            if module.name() == name:
523                return module
524        return None
525
526    def enabled(self):
527        return self._enabled
528
529    def disabled(self):
530        return self._disabled
531
532    def modules(self):
533        return self._modules
534
535    def configured(self):
536        return self._configured
537