Mercurial > bitten > bitten-test
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)