# HG changeset patch # User cmlenz # Date 1118073269 0 # Node ID 196009657e5e6f0977923daa04e07f189c336631 # Parent 9ac0ee86ec7cdcbcd8b0e5997701fcf839fcd4a4 Simplify the recipe commands interface: * The implementation of a command is now located using a pseudo-protocol for namespace URIs. * Commands are simply module-level functions instead of components. * Remove dependency of the recipe/slave code on the Trac component architecture. diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ PYLINT_MSGS = C0101,E0201,E0213,W0103,W0704,R0921,R0923 +PYTHONPATH = . all: pylint pylint: - PYTHONPATH=.:../Trac/trunk pylint --parseable=y --disable-msg=$(PYLINT_MSGS) --ignore=tests bitten > build/pylint-results.txt + PYTHONPATH=$(PYTHONPATH) pylint --parseable=y --disable-msg=$(PYLINT_MSGS) --ignore=tests bitten > build/pylint-results.txt diff --git a/bitten/general/__init__.py b/bitten/general/__init__.py deleted file mode 100644 diff --git a/bitten/general/cmd_make.py b/bitten/general/cmd_make.py deleted file mode 100644 --- a/bitten/general/cmd_make.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -from trac.core import * -from trac.util import NaivePopen -from bitten import BuildError -from bitten.recipe import ICommandExecutor - - -class MakeExecutor(Component): - implements(ICommandExecutor) - - def get_name(self): - return 'make' - - def execute(self, basedir, target='all'): - cmd = NaivePopen('make %s' % target, capturestderr=True) - for line in cmd.out.splitlines(): - print '[make] %s' % line diff --git a/bitten/python/__init__.py b/bitten/python/__init__.py deleted file mode 100644 diff --git a/bitten/python/cmd_distutils.py b/bitten/python/cmd_distutils.py deleted file mode 100644 --- a/bitten/python/cmd_distutils.py +++ /dev/null @@ -1,21 +0,0 @@ -import os - -from trac.core import * -from trac.util import NaivePopen -from bitten import BuildError -from bitten.recipe import ICommandExecutor - - -class DistutilsExecutor(Component): - implements(ICommandExecutor) - - def get_name(self): - return 'distutils' - - def execute(self, basedir, command='build'): - try: - cmd = NaivePopen('python setup.py %s' % command) - for line in cmd.out.splitlines(): - print '[distutils] %s' % line - except OSError, e: - raise BuildError, 'Executing distutils failed: %s' % e diff --git a/bitten/python/rep_pylint.py b/bitten/python/rep_pylint.py deleted file mode 100644 --- a/bitten/python/rep_pylint.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import re - -from elementtree import ElementTree -from trac.core import * -from bitten import BuildError -from bitten.recipe import IReportProcessor - - -_msg_re = re.compile(r'^(?P.+):(?P\d+): ' - r'\[(?P[A-Z])(?:, (?P[\w\.]+))?\] ' - r'(?P.*)$') - -class PylintReportProcessor(Component): - implements(IReportProcessor) - - def get_name(self): - return 'pylint' - - def process(self, basedir, file=None): - assert file, 'Missing required attribute "file"' - - for line in open(file, 'r'): - match = _msg_re.search(line) - if match: - filename = match.group('file') - if filename.startswith(basedir): - filename = filename[len(basedir) + 1:] - lineno = int(match.group('line')) diff --git a/bitten/python/rep_trace.py b/bitten/python/rep_trace.py deleted file mode 100644 --- a/bitten/python/rep_trace.py +++ /dev/null @@ -1,19 +0,0 @@ -import os -import re - -from elementtree import ElementTree -from trac.core import * -from bitten import BuildError -from bitten.recipe import IReportProcessor - - -class TraceReportProcessor(Component): - implements(IReportProcessor) - - def get_name(self): - return 'trace' - - def process(self, basedir, summary=None, coverdir=None, include=None, - exclude=None): - assert summary, 'Missing required attribute "summary"' - assert coverdir, 'Missing required attribute "coverdir"' diff --git a/bitten/python/rep_unittest.py b/bitten/python/rep_unittest.py deleted file mode 100644 --- a/bitten/python/rep_unittest.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import re - -from elementtree import ElementTree -from trac.core import * -from bitten import BuildError -from bitten.recipe import IReportProcessor - - -_test_re = re.compile(r'^(?P\w+) \((?P\d+): ' - r'\[(?P[A-Z])(?:, (?P[\w\.]+))?\] ' - r'(?P.*)$') - -class UnittestReportProcessor(Component): - implements(IReportProcessor) - - def get_name(self): - return 'unittest' - - def process(self, basedir, file=None): - assert file, 'Missing required attribute "file"' diff --git a/bitten/recipe/__init__.py b/bitten/recipe/__init__.py new file mode 100644 --- /dev/null +++ b/bitten/recipe/__init__.py @@ -0,0 +1,1 @@ +from bitten.recipe.api import * \ No newline at end of file diff --git a/bitten/recipe.py b/bitten/recipe/api.py rename from bitten/recipe.py rename to bitten/recipe/api.py --- a/bitten/recipe.py +++ b/bitten/recipe/api.py @@ -1,71 +1,63 @@ -import os import os.path -from elementtree import ElementTree +from xml.dom import minidom -from trac.core import * +from bitten import BuildError + +__all__ = ['Recipe'] -class ICommandExecutor(Interface): - - def get_name(): - """ - Return the name of the command as used in the XML file. - """ - - def execute(basedir, **attrs): - """ - """ +class Step(object): + """Represents a single step of a build recipe. - -class IReportProcessor(Interface): + Iterate over an object of this class to get the commands to execute, and + their keyword arguments. + """ - def get_name(): - """ - Return the name of the command as used in the XML file. - """ + def __init__(self, node): + self._node = node + self.id = node.getAttribute('id') + self.description = node.getAttribute('description') - def process(basedir, **attrs): - """ - """ + def __iter__(self): + for child in [c for c in self._node.childNodes if c.nodeType == 1]: + if child.namespaceURI: + # Commands + yield self._translate(child) + elif child.tagName == 'reports': + # Reports + for child in [c for c in child.childNodes if c.nodeType == 1]: + yield self._translate(child) + else: + raise BuildError, "Unknown element <%s>" % child.tagName + + def _translate(self, node): + if not node.namespaceURI.startswith('bitten:'): + # Ignore elements in a foreign namespace + return None + + module = __import__(node.namespaceURI[7:], globals(), locals(), + node.localName) + func = getattr(module, node.localName) + attrs = {} + for name, value in node.attributes.items(): + attrs[name.encode()] = value.encode() + return func, attrs class Recipe(object): + """Represents a build recipe. + + Iterate over this object to get the individual build steps in the order they + have been defined in the recipe file.""" def __init__(self, filename='recipe.xml', basedir=os.getcwd()): self.filename = filename self.basedir = basedir self.path = os.path.join(basedir, filename) - self.tree = ElementTree.parse(self.path).getroot() - - description = property(fget=lambda self: self.tree.attrib['description']) - - -class RecipeExecutor(Component): - - command_executors = ExtensionPoint(ICommandExecutor) - report_processors = ExtensionPoint(IReportProcessor) + self.root = minidom.parse(self.path).documentElement + self.description = self.root.getAttribute('description') - def execute(self, recipe): - for step in recipe.tree: - print '---> %s' % step.attrib['title'] - for element in step: - if element.tag == 'reports': - for report in element: - reporter = self._get_report_processor(report.tag) - reporter.process(recipe.basedir, **report.attrib) - else: - cmd = self._get_command_executor(element.tag) - cmd.execute(recipe.basedir, **element.attrib) - print - - def _get_command_executor(self, name): - for command_executor in self.command_executors: - if command_executor.get_name() == name: - return command_executor - raise Exception, "Unknown command <%s>" % name - - def _get_report_processor(self, name): - for report_processor in self.report_processors: - if report_processor.get_name() == name: - return report_processor - raise Exception, "Unknown report type <%s>" % name + def __iter__(self): + """Provide an iterator over the individual steps of the recipe.""" + for child in self.root.getElementsByTagName('step'): + yield Step(child) diff --git a/bitten/recipe/ctools.py b/bitten/recipe/ctools.py new file mode 100644 --- /dev/null +++ b/bitten/recipe/ctools.py @@ -0,0 +1,18 @@ +from popen2 import Popen3 + +def make(basedir, target='all'): + """Execute a Makefile target.""" + cmdline = 'make %s' % target + pipe = Popen3(cmdline, capturestderr=True) # FIXME: Windows compatibility + while True: + retval = pipe.poll() + if retval != -1: + break + line = pipe.fromchild.readline() + if line: + print '[make] %s' % line.rstrip() + line = pipe.childerr.readline() + if line: + print '[make] %s' % line.rstrip() + if retval != 0: + raise BuildError, "Executing distutils failed (%s)" % retval diff --git a/bitten/recipe/pythontools.py b/bitten/recipe/pythontools.py new file mode 100644 --- /dev/null +++ b/bitten/recipe/pythontools.py @@ -0,0 +1,44 @@ +import re +from popen2 import Popen3 + +from bitten import BuildError + +def distutils(basedir, command='build'): + """Execute a `distutils` command.""" + cmdline = 'python setup.py %s' % command + pipe = Popen3(cmdline, capturestderr=True) # FIXME: Windows compatibility + while True: + retval = pipe.poll() + if retval != -1: + break + line = pipe.fromchild.readline() + if line: + print '[distutils] %s' % line.rstrip() + line = pipe.childerr.readline() + if line: + print '[distutils] %s' % line.rstrip() + if retval != 0: + raise BuildError, "Executing distutils failed (%s)" % retval + +def pylint(basedir, file=None): + """Extract data from a `pylint` run written to a file.""" + assert file, 'Missing required attribute "file"' + _msg_re = re.compile(r'^(?P.+):(?P\d+): ' + r'\[(?P[A-Z])(?:, (?P[\w\.]+))?\] ' + r'(?P.*)$') + for line in open(file, 'r'): + match = _msg_re.search(line) + if match: + filename = match.group('file') + if filename.startswith(basedir): + filename = filename[len(basedir) + 1:] + lineno = int(match.group('line')) + +def trace(basedir, summary=None, coverdir=None, include=None, exclude=None): + """Extract data from a `trac.py` run.""" + assert summary, 'Missing required attribute "summary"' + assert coverdir, 'Missing required attribute "coverdir"' + +def unittest(basedir, file=None): + """Extract data from a unittest results file in XML format.""" + assert file, 'Missing required attribute "file"' diff --git a/bitten/recipe/tests/__init__.py b/bitten/recipe/tests/__init__.py new file mode 100644 --- /dev/null +++ b/bitten/recipe/tests/__init__.py @@ -0,0 +1,8 @@ +import unittest + +from bitten.recipe.tests import api + +def suite(): + suite = unittest.TestSuite() + suite.addTest(api.suite()) + return suite diff --git a/bitten/recipe/tests/api.py b/bitten/recipe/tests/api.py new file mode 100644 --- /dev/null +++ b/bitten/recipe/tests/api.py @@ -0,0 +1,27 @@ +import os +import os.path +import tempfile +import unittest + +from bitten.recipe import Recipe + + +class RecipeTestCase(unittest.TestCase): + + def setUp(self): + self.temp_dir = tempfile.gettempdir() + self.recipe_xml = open(os.path.join(self.temp_dir, 'recipe.xml'), 'w') + + def tearDown(self): + os.unlink(os.path.join(self.temp_dir, 'recipe.xml')) + + def testDescription(self): + self.recipe_xml.write('' + '' + '') + self.recipe_xml.close() + recipe = Recipe(basedir=self.temp_dir) + self.assertEqual('test', recipe.description) + +def suite(): + return unittest.makeSuite(RecipeTestCase, 'test') diff --git a/bitten/tests/__init__.py b/bitten/tests/__init__.py --- a/bitten/tests/__init__.py +++ b/bitten/tests/__init__.py @@ -1,6 +1,6 @@ import unittest -from bitten.tests import recipe +from bitten.recipe import tests as recipe def suite(): suite = unittest.TestSuite() diff --git a/bitten/tests/recipe.py b/bitten/tests/recipe.py deleted file mode 100644 --- a/bitten/tests/recipe.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import os.path -import tempfile -import unittest - -from bitten.recipe import Recipe - - -class RecipeTestCase(unittest.TestCase): - - def setUp(self): - self.temp_dir = tempfile.gettempdir() - self.recipe_xml = open(os.path.join(self.temp_dir, 'recipe.xml'), 'w') - - def tearDown(self): - os.unlink(os.path.join(self.temp_dir, 'recipe.xml')) - - def testDescription(self): - self.recipe_xml.write('' - '' - '') - self.recipe_xml.close() - recipe = Recipe(basedir=self.temp_dir) - self.assertEqual('test', recipe.description) - -def suite(): - return unittest.makeSuite(RecipeTestCase, 'test') diff --git a/recipe.xml b/recipe.xml --- a/recipe.xml +++ b/recipe.xml @@ -1,30 +1,32 @@ - + - + - + - - + + - + - + - + diff --git a/scripts/build.py b/scripts/build.py --- a/scripts/build.py +++ b/scripts/build.py @@ -1,12 +1,31 @@ #!/usr/bin/env python -from trac.core import ComponentManager +import sys -from bitten.recipe import Recipe, RecipeExecutor -from bitten.python import cmd_distutils, rep_pylint, rep_unittest, rep_trace -from bitten.general import cmd_make +from bitten import BuildError +from bitten.recipe import Recipe + +def build(): + step_id = None + if len(sys.argv) > 1: + step_id = sys.argv[1] + + recipe = Recipe() + steps_run = [] + for step in recipe: + if not step_id or step.id == step_id: + print '-->', step.description or step.id + for function, kw in step: + function(recipe.basedir, **kw) + print + steps_run.append(step.id) + + if step_id and not step_id in steps_run: + raise BuildError, "Recipe has no step named '%s'" % step_id if __name__ == '__main__': - mgr = ComponentManager() - recipe = Recipe() - RecipeExecutor(mgr).execute(recipe) + try: + build() + except BuildError, e: + print>>sys.stderr, "FAILED: %s" % e + sys.exit(-1) diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + from distutils.core import setup, Command from bitten.distutils.testrunner import unittest