changeset 233:8f816147620f

* Moved SlaveConfiguration logic into new module ([source:/trunk/bitten/build/config.py bitten.build.config]). * The configuration properties can now be used by recipe commands. For example, the Python tools use the Python interpreter specified by the `python.path` property, if specified. * A build recipe can reference configuration in command attributes, using the notation `${property.name:default value}`.
author cmlenz
date Fri, 30 Sep 2005 15:42:50 +0000
parents b6e4896dc026
children 7297fdb90255
files bitten/build/config.py bitten/build/pythontools.py bitten/build/tests/__init__.py bitten/build/tests/config.py bitten/recipe.py bitten/slave.py
diffstat 6 files changed, 299 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bitten/build/config.py
@@ -0,0 +1,122 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+
+from ConfigParser import SafeConfigParser
+import os
+import platform
+import re
+
+
+class Configuration(object):
+    """Encapsulates the configuration of a build machine.
+    
+    Configuration values can be provided through a configuration file (in INI
+    format) or through command-line parameters (properties). In addition to
+    explicitly defined properties, this class automatically collects platform
+    information and stores them as properties. These defaults can be
+    overridden (useful for cross-compilation).
+    """
+    # TODO: document mapping from config file to property names
+
+    def __init__(self, filename=None, properties=None):
+        """Create the configuration object.
+        
+        @param filename: The path to the configuration file, if any
+        @param properties: A dictionary of the configuration properties
+                           provided on the command-line
+        """
+        self.properties = {}
+        self.packages = {}
+        parser = SafeConfigParser()
+        if filename:
+            parser.read(filename)
+        self._merge_sysinfo(parser, properties)
+        self._merge_packages(parser, properties)
+
+    def _merge_sysinfo(self, parser, properties):
+        """Merge the platform information properties into the configuration."""
+        system, node, release, version, machine, processor = platform.uname()
+        system, release, version = platform.system_alias(system, release,
+                                                         version)
+        self.properties['machine'] = machine
+        self.properties['processor'] = processor
+        self.properties['os'] = system
+        self.properties['family'] = os.name
+        self.properties['version'] = release
+
+        mapping = {'machine': ('machine', 'name'),
+                   'processor': ('machine', 'processor'),
+                   'os': ('os', 'name'),
+                   'family': ('os', 'family'),
+                   'version': ('os', 'version')}
+        for key, (section, option) in mapping.items():
+            if parser.has_section(section):
+                value = parser.get(section, option)
+                if value is not None:
+                    self.properties[key] = value
+
+        if properties:
+            for key, value in properties.items():
+                if key in mapping:
+                    self.properties[key] = value
+
+    def _merge_packages(self, parser, properties):
+        """Merge package information into the configuration."""
+        for section in parser.sections():
+            if section in ('os', 'machine', 'maintainer'):
+                continue
+            package = {}
+            for option in parser.options(section):
+                package[option] = parser.get(section, option)
+            self.packages[section] = package
+
+        if properties:
+            for key, value in properties.items():
+                if '.' in key:
+                    package, propname = key.split('.', 1)
+                    if package not in self.packages:
+                        self.packages[package] = {}
+                    self.packages[package][propname] = value
+
+    def __contains__(self, key):
+        if '.' in key:
+            package, propname = key.split('.', 1)
+            return propname in self.packages.get(package, {})
+        return key in self.properties
+
+    def __getitem__(self, key):
+        if '.' in key:
+            package, propname = key.split('.', 1)
+            return self.packages.get(package, {}).get(propname)
+        return self.properties.get(key)
+
+    def __str__(self):
+        return str({'properties': self.properties, 'packages': self.packages})
+
+    _VAR_RE = re.compile(r'\$\{(?P<ref>\w[\w.]*?\w)(?:\:(?P<def>.+))?\}')
+
+    def interpolate(self, text):
+        """Interpolate configuration properties into a string.
+        
+        Properties can be referenced in the text using the notation
+        `${property.name}`. A default value can be provided by appending it to
+        the property name separated by a colon, for example
+        `${property.name:defaultvalue}`. This value will be used when there's
+        no such property in the configuration. Otherwise, if no default is
+        provided, the reference is not replaced at all.
+        """
+        def _replace(m):
+            refname = m.group('ref')
+            if refname in self:
+                return self[refname]
+            elif m.group('def'):
+                return m.group('def')
+            else:
+                return m.group(0)
+        return self._VAR_RE.sub(_replace, text)
--- a/bitten/build/pythontools.py
+++ b/bitten/build/pythontools.py
@@ -14,15 +14,31 @@
     set
 except NameError:
     from sets import Set as set
+import sys
 
 from bitten.build import CommandLine, FileSet
 from bitten.util import loc, xmlio
 
 log = logging.getLogger('bitten.build.pythontools')
 
-def distutils(ctxt, command='build'):
+def _python_path(ctxt):
+    """Return the path to the Python interpreter.
+    
+    If the configuration has a `python.path` property, the value of that option
+    is returned; otherwise the path to the current Python interpreter is
+    returned.
+    """
+    python_path = ctxt.config['python.path']
+    if python_path:
+        if os.path.isfile(python_path):
+            return python_path
+        log.warning('Invalid python.path: %s is not a file')
+    return sys.executable
+
+def distutils(ctxt, command='build', file_='setup.py'):
     """Execute a `distutils` command."""
-    cmdline = CommandLine('python', ['setup.py', command], cwd=ctxt.basedir)
+    cmdline = CommandLine(_python_path(ctxt), [ctxt.resolve(file_), command],
+                          cwd=ctxt.basedir)
     log_elem = xmlio.Fragment()
     for out, err in cmdline.execute():
         if out is not None:
@@ -58,8 +74,8 @@
             return
 
     from bitten.build import shtools
-    shtools.exec_(ctxt, executable='python', file_=file_, output=output,
-                  args=args)
+    shtools.exec_(ctxt, executable=_python_path(ctxt), file_=file_,
+                  output=output, args=args)
 
 def pylint(ctxt, file_=None):
     """Extract data from a `pylint` run written to a file."""
--- a/bitten/build/tests/__init__.py
+++ b/bitten/build/tests/__init__.py
@@ -9,11 +9,12 @@
 
 import unittest
 
-from bitten.build.tests import api, pythontools
+from bitten.build.tests import api, config, pythontools
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(api.suite())
+    suite.addTest(config.suite())
     suite.addTest(pythontools.suite())
     return suite
 
new file mode 100644
--- /dev/null
+++ b/bitten/build/tests/config.py
@@ -0,0 +1,128 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://bitten.cmlenz.net/wiki/License.
+
+import platform
+import os
+import shutil
+import tempfile
+import unittest
+
+from bitten.build.config import Configuration
+
+
+class ConfigurationTestCase(unittest.TestCase):
+
+    def test_sysinfo_defaults(self):
+        config = Configuration()
+
+        self.assertEqual(platform.machine(), config['machine'])
+        self.assertEqual(platform.processor(), config['processor'])
+        system, release, version = platform.system_alias(platform.system(),
+                                                         platform.release(),
+                                                         platform.version())
+        self.assertEqual(system, config['os'])
+        self.assertEqual(os.name, config['family'])
+        self.assertEqual(release, config['version'])
+
+    def test_sysinfo_properties_override(self):
+        config = Configuration(properties={
+            'machine': 'MACHINE',
+            'processor': 'PROCESSOR',
+            'os': 'OS',
+            'family': 'FAMILY',
+            'version': 'VERSION'
+        })
+        self.assertEqual('MACHINE', config['machine'])
+        self.assertEqual('PROCESSOR', config['processor'])
+        self.assertEqual('OS', config['os'])
+        self.assertEqual('FAMILY', config['family'])
+        self.assertEqual('VERSION', config['version'])
+
+    def test_sysinfo_configfile_override(self):
+        inifile, ininame = tempfile.mkstemp(prefix='bitten_test')
+        try:
+            os.write(inifile, """
+[machine]
+name = MACHINE
+processor = PROCESSOR
+
+[os]
+name = OS
+family = FAMILY
+version = VERSION
+""")
+            os.close(inifile)
+            config = Configuration(ininame)
+
+            self.assertEqual('MACHINE', config['machine'])
+            self.assertEqual('PROCESSOR', config['processor'])
+            self.assertEqual('OS', config['os'])
+            self.assertEqual('FAMILY', config['family'])
+            self.assertEqual('VERSION', config['version'])
+        finally:
+            os.remove(ininame)
+
+    def test_package_properties(self):
+        config = Configuration(properties={
+            'python.version': '2.3.5',
+            'python.path': '/usr/local/bin/python2.3'
+        })
+        self.assertEqual(True, 'python' in config.packages)
+        self.assertEqual('/usr/local/bin/python2.3', config['python.path'])
+        self.assertEqual('2.3.5', config['python.version'])
+
+    def test_package_configfile(self):
+        inifile, ininame = tempfile.mkstemp(prefix='bitten_test')
+        try:
+            os.write(inifile, """
+[python]
+path = /usr/local/bin/python2.3
+version = 2.3.5
+""")
+            os.close(inifile)
+            config = Configuration(ininame)
+
+            self.assertEqual(True, 'python' in config.packages)
+            self.assertEqual('/usr/local/bin/python2.3', config['python.path'])
+            self.assertEqual('2.3.5', config['python.version'])
+        finally:
+            os.remove(ininame)
+
+    def test_interpolate(self):
+        config = Configuration(properties={
+            'python.version': '2.3.5',
+            'python.path': '/usr/local/bin/python2.3'
+        })
+        self.assertEqual('/usr/local/bin/python2.3',
+                         config.interpolate('${python.path}'))
+        self.assertEqual('foo /usr/local/bin/python2.3 bar',
+                         config.interpolate('foo ${python.path} bar'))
+
+    def test_interpolate_default(self):
+        config = Configuration()
+        self.assertEqual('python2.3',
+                         config.interpolate('${python.path:python2.3}'))
+        self.assertEqual('foo python2.3 bar',
+                         config.interpolate('foo ${python.path:python2.3} bar'))
+
+    def test_interpolate_missing(self):
+        config = Configuration()
+        self.assertEqual('${python.path}',
+                         config.interpolate('${python.path}'))
+        self.assertEqual('foo ${python.path} bar',
+                         config.interpolate('foo ${python.path} bar'))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(ConfigurationTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
--- a/bitten/recipe.py
+++ b/bitten/recipe.py
@@ -10,6 +10,7 @@
 import keyword
 import logging
 import os
+import re
 
 from pkg_resources import WorkingSet
 from bitten.build import BuildError
@@ -30,8 +31,9 @@
     step = None # The current step
     generator = None # The current generator (namespace#name)
 
-    def __init__(self, basedir):
+    def __init__(self, basedir, config=None):
         self.basedir = os.path.realpath(basedir)
+        self.config = config or {}
         self.output = []
 
     def run(self, step, namespace, name, attr):
@@ -53,7 +55,8 @@
             if keyword.iskeyword(name) or name in __builtins__:
                 name = name + '_'
             return name
-        args = dict([(escape(name), attr[name]) for name in attr])
+        args = dict([(escape(name), self.config.interpolate(attr[name]))
+                     for name in attr])
 
         self.step = step
         self.generator = qname
@@ -116,9 +119,9 @@
     LOG = 'log'
     REPORT = 'report'
 
-    def __init__(self, xml, basedir=os.getcwd()):
+    def __init__(self, xml, basedir=os.getcwd(), config=None):
         assert isinstance(xml, xmlio.ParsedElement)
-        self.ctxt = Context(basedir)
+        self.ctxt = Context(basedir, config)
         self._root = xml
         self.description = self._root.attr.get('description')
 
--- a/bitten/slave.py
+++ b/bitten/slave.py
@@ -12,10 +12,15 @@
 import logging
 import os
 import platform
+try:
+    set
+except NameError:
+    from sets import Set as set
 import shutil
 import tempfile
 
 from bitten.build import BuildError
+from bitten.build.config import Configuration
 from bitten.recipe import Recipe, InvalidRecipeError
 from bitten.util import archive, beep, xmlio
 
@@ -65,41 +70,26 @@
                 raise beep.TerminateSession, 'Registration failed!'
             log.info('Registration successful')
 
-        family = os.name
-        system, node, release, version, machine, processor = platform.uname()
-        system, release, version = platform.system_alias(system, release,
-                                                         version)
+        self.config = Configuration(self.session.config)
         if self.session.name is not None:
             node = self.session.name
         else:
-            node = node.split('.', 1)[0].lower()
-
-        packages = []
-        if self.session.config is not None:
-            log.debug('Merging configuration from %s', self.session.config)
-            config = ConfigParser()
-            config.read(self.session.config)
-            for section in config.sections():
-                if section == 'machine':
-                    machine = config.get(section, 'name', machine)
-                    processor = config.get(section, 'processor', processor)
-                elif section == 'os':
-                    system = config.get(section, 'name', system)
-                    family = config.get(section, 'family', os.name)
-                    release = config.get(section, 'version', release)
-                else: # a package
-                    attrs = {}
-                    for option in config.options(section):
-                        attrs[option] = config.get(section, option)
-                    packages.append(xmlio.Element('package', name=section,
-                                                  **attrs))
+            node = platform.node().split('.', 1)[0].lower()
 
         log.info('Registering with build master as %s', node)
+        log.debug('Properties: %s', self.config.properties)
         xml = xmlio.Element('register', name=node)[
-            xmlio.Element('platform', processor=processor)[machine],
-            xmlio.Element('os', family=family, version=release)[system],
-            xmlio.Fragment()[packages]
+            xmlio.Element('platform', processor=self.config['processor'])[
+                self.config['machine']
+            ],
+            xmlio.Element('os', family=self.config['family'],
+                                version=self.config['release'])[
+                self.config['os']
+            ],
         ]
+        for package, properties in self.config.packages.items():
+            xml.append(xmlio.Element('package', name=package, **properties))
+
         self.channel.send_msg(beep.Payload(xml), handle_reply)
 
     def handle_msg(self, msgno, payload):
@@ -157,7 +147,8 @@
                 return
 
             try:
-                self.execute_build(msgno, Recipe(self.recipe_xml, path))
+                recipe = Recipe(self.recipe_xml, path, self.config)
+                self.execute_build(msgno, recipe)
             finally:
                 shutil.rmtree(path)
                 os.unlink(archive_path)
Copyright (C) 2012-2017 Edgewall Software