From 9affb95b798d52b619905accfa181e2940cb27b5 Mon Sep 17 00:00:00 2001 From: Jim Fulton <jim@zope.com> Date: Mon, 5 Jun 2006 11:25:19 +0000 Subject: [PATCH] Checking in initial work. Still need more tests and features. --- README.txt | 58 +++ bootstrap.py | 66 ++++ buildout.cfg | 8 + eggrecipe/README.txt | 1 + eggrecipe/setup.py | 19 + eggrecipe/src/zc/__init__.py | 1 + eggrecipe/src/zc/recipe/__init__.py | 1 + eggrecipe/src/zc/recipe/egg/README.txt | 94 +++++ eggrecipe/src/zc/recipe/egg/__init__.py | 1 + eggrecipe/src/zc/recipe/egg/egg.py | 50 +++ eggrecipe/src/zc/recipe/egg/tests.py | 100 +++++ setup.py | 19 + src/zc/__init__.py | 5 + src/zc/buildout/__init__.py | 1 + src/zc/buildout/build.py | 320 ++++++++++++++++ src/zc/buildout/buildout.txt | 363 +++++++++++++++++++ src/zc/buildout/easy_install.py | 54 +++ src/zc/buildout/egglinker.py | 111 ++++++ src/zc/buildout/testing.py | 109 ++++++ src/zc/buildout/tests.py | 35 ++ testrunnerrecipe/README.txt | 1 + testrunnerrecipe/setup.py | 20 + testrunnerrecipe/src/zc/__init__.py | 1 + testrunnerrecipe/src/zc/recipe/__init__.py | 1 + testrunnerrecipe/src/zc/recipe/testrunner.py | 63 ++++ todo.txt | 40 ++ 26 files changed, 1542 insertions(+) create mode 100644 README.txt create mode 100644 bootstrap.py create mode 100644 buildout.cfg create mode 100644 eggrecipe/README.txt create mode 100644 eggrecipe/setup.py create mode 100644 eggrecipe/src/zc/__init__.py create mode 100644 eggrecipe/src/zc/recipe/__init__.py create mode 100644 eggrecipe/src/zc/recipe/egg/README.txt create mode 100644 eggrecipe/src/zc/recipe/egg/__init__.py create mode 100644 eggrecipe/src/zc/recipe/egg/egg.py create mode 100644 eggrecipe/src/zc/recipe/egg/tests.py create mode 100644 setup.py create mode 100644 src/zc/__init__.py create mode 100644 src/zc/buildout/__init__.py create mode 100644 src/zc/buildout/build.py create mode 100644 src/zc/buildout/buildout.txt create mode 100644 src/zc/buildout/easy_install.py create mode 100644 src/zc/buildout/egglinker.py create mode 100644 src/zc/buildout/testing.py create mode 100644 src/zc/buildout/tests.py create mode 100644 testrunnerrecipe/README.txt create mode 100644 testrunnerrecipe/setup.py create mode 100644 testrunnerrecipe/src/zc/__init__.py create mode 100644 testrunnerrecipe/src/zc/recipe/__init__.py create mode 100644 testrunnerrecipe/src/zc/recipe/testrunner.py create mode 100644 todo.txt diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..f5081d1d --- /dev/null +++ b/README.txt @@ -0,0 +1,58 @@ +Zope Buildout +============= + +The Zope Buildout project provides support for creating applications, +especially Pyton applications. It provides tools for assembling +applications from multiple parts, Python or otherwise. An application +may actually contain multiple programs, processes, and configuration +settings. + +The word "buildout" refers to a description of a set of parts and the +software to create ans assemble them. It is often used informally to +refer to an installed system based on a buildout definition. For +example, if we are creating an application named "Foo", then "the Foo +buildout" is the collection of configuration and application-specific +software that allows an instance of the application to be created. We +may refer to such an instance of the application informally as "a Foo +buildout". + +I expect that, for many Zope packages, we'll arrange the package +projects in subversion as buildouts. To work on the package, someone +will check the project out of Subversion and build it. Building it +will assemble all of packages and progras needed to work on it. For +example, a buildout for a project to provide a new security policy +will include the source of the policy and specifications to build the +application for working on it, including: + +- a test runner + +- a web server for running the user interface + +- supporting packages + +A buildout will typically contain a copy of bootstrap.py. When +someone checks out the project, they'll run bootstrap.py, which will + +- create support directories, like bin, eggs, and work, as needed, + +- download and install the zc.buildout and setuptools eggs, + +- run bin/build (created by installing zc.buildout) to build the + application. + +Buildouts are defined using configuration files. These files are +based on the Python ConfigParser module with some variable-definition +and substitution extensions. + +The detailed documentation for the various parts of bukdout can be +found in the following files: + +bootstrap.txt + Describes how to use the bootstrapping script + +buildout.txt + Describes how to define and run buildouts. It also describes how + to write recipes. + +recipes.txt + Documents the few built-in recipes. diff --git a/bootstrap.py b/bootstrap.py new file mode 100644 index 00000000..62763b25 --- /dev/null +++ b/bootstrap.py @@ -0,0 +1,66 @@ +############################################################################## +# +# Copyright (c) 2005 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout + +$Id$ +""" + +import os, sys, urllib2 + +for d in 'eggs', 'bin': + if not os.path.exists(d): + os.mkdir(d) + +ez = {} +exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' + ).read() in ez + +ez['use_setuptools'](to_dir='eggs', download_delay=0) + +import setuptools.command.easy_install +import pkg_resources +import setuptools.package_index +import distutils.dist + +os.spawnle(os.P_WAIT, sys.executable, sys.executable, 'setup.py', + '-q', 'develop', '-m', '-x', '-d', 'eggs', + {'PYTHONPATH': os.path.dirname(pkg_resources.__file__)}, + ) + +## easy = setuptools.command.easy_install.easy_install( +## distutils.dist.Distribution(), +## multi_version=True, +## exclude_scripts=True, +## sitepy_installed=True, +## install_dir='eggs', +## outputs=[], +## quiet=True, +## zip_ok=True, +## args=['zc.buildout'], +## ) +## easy.finalize_options() +## easy.easy_install('zc.buildout') + +env = pkg_resources.Environment(['eggs']) + +ws = pkg_resources.WorkingSet() +sys.path[0:0] = [ + d.location + for d in ws.resolve([pkg_resources.Requirement.parse('zc.buildout')], env) + ] + +import zc.buildout.egglinker +zc.buildout.egglinker.scripts(['zc.buildout'], 'bin', ['eggs']) + +sys.exit(os.spawnl(os.P_WAIT, 'bin/buildout', 'bin/buildout')) diff --git a/buildout.cfg b/buildout.cfg new file mode 100644 index 00000000..1577370c --- /dev/null +++ b/buildout.cfg @@ -0,0 +1,8 @@ +[buildout] +develop = eggrecipe testrunnerrecipe +parts = test + +[test] +recipe = zc.recipe.testrunner +distributions = zc.buildout zc.recipe.egg + diff --git a/eggrecipe/README.txt b/eggrecipe/README.txt new file mode 100644 index 00000000..07c70649 --- /dev/null +++ b/eggrecipe/README.txt @@ -0,0 +1 @@ +Buildout recipe for installing Python distutils distributions as eggs diff --git a/eggrecipe/setup.py b/eggrecipe/setup.py new file mode 100644 index 00000000..3e00da9e --- /dev/null +++ b/eggrecipe/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup, find_packages + +setup( + name = "zc.recipe.egg", + version = "0.1", + packages = find_packages('src'), + include_package_data = True, + package_dir = {'':'src'}, + namespace_packages = ['zc', 'zc.recipe'], + install_requires = ['zc.buildout'], + tests_require = ['zope.testing'], + test_suite = 'zc.recipe.eggs.tests.test_suite', + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "Recipe for installing Python package distributions as eggs", + license = "ZPL 2.1", + keywords = "development build", + entry_points = {'zc.buildout': ['default = zc.recipe.egg:Egg']}, + ) diff --git a/eggrecipe/src/zc/__init__.py b/eggrecipe/src/zc/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/eggrecipe/src/zc/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/eggrecipe/src/zc/recipe/__init__.py b/eggrecipe/src/zc/recipe/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/eggrecipe/src/zc/recipe/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/eggrecipe/src/zc/recipe/egg/README.txt b/eggrecipe/src/zc/recipe/egg/README.txt new file mode 100644 index 00000000..c1688bcf --- /dev/null +++ b/eggrecipe/src/zc/recipe/egg/README.txt @@ -0,0 +1,94 @@ +Installation of distributions as eggs +===================================== + +The zc.recipe.egg ewcipe can be used to install various types if +distutils distributions as eggs. It takes a number of options: + +distribution + The distribution specifies the distribution requirement. + + This is a requirement as defined by setuptools. + +find_links + A list of URLs, files, or directories to search for distributions. + +To illustrate this, we've created a directory with some sample eggs: + + >>> ls(sample_eggs) + - demo-0.1-py2.3.egg + - demo-0.2-py2.3.egg + - demo-0.3-py2.3.egg + - demoneeded-1.0-py2.3.egg + +We have a sample buildout. Let's update it's configuration file to +install the demo package. + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... + ... [demo] + ... recipe = zc.recipe.egg + ... distribution = demo <0.3 + ... find_links = %s + ... """ % sample_eggs) + +In this example, we limited ourself to revisions before 0.3. We also +specified where to find distributions using the find_links option. + +Let's run the buildout: + + >>> import os + >>> os.chdir(sample_buildout) + >>> runscript = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(runscript), + +Now, if we look at the buildout eggs directory: + + >>> ls(sample_buildout, 'eggs') + - demo-0.2-py2.3.egg + - demoneeded-1.0-py2.3.egg + - zc.recipe.egg.egg-link + +We see that we got an egg for demo that met the requirement, as well +as the egg for demoneeded, wich demo requires. (We also see an egg +link for the recipe. This egg link was actually created as part of +the sample buildout setup. Normally, when using the recipe, you'll get +a regular egg installation.) + +The demo egg also defined a script and we see that the script was +installed as well: + + >>> ls(sample_buildout, 'bin') + - buildout + - demo + - py_demo + +Here, in addition to the buildout script, we see the demo script, +demo, and we see a script, py_demo, for giving us a Python prompt with +the path for demo and any eggs it depends on included in sys.path. +This is useful for testing. + +If we run the demo script, it prints out some minimal data: + + >>> print system(os.path.join(sample_buildout, 'bin', 'demo')), + 2 1 + +The value it prints out happens to be some values defined in the +modules installed. + +We can also run the py_demo script. Here we'll just print out +the bits if the path added to reflect the eggs: + + >>> print system(os.path.join(sample_buildout, 'bin', 'py_demo'), + ... """for p in sys.path[:3]: + ... print p + ... """).replace('>>> ', '').replace('... ', ''), + ... # doctest: +ELLIPSIS + <BLANKLINE> + /usr/local/python/2.3.5/lib/python/setuptools-0.6b2-py2.3.egg + /tmp/tmpcy8MvGbuildout-tests/eggs/demo-0.2-py2.3.egg + /tmp/tmpcy8MvGbuildout-tests/eggs/demoneeded-1.0-py2.3.egg + <BLANKLINE> + diff --git a/eggrecipe/src/zc/recipe/egg/__init__.py b/eggrecipe/src/zc/recipe/egg/__init__.py new file mode 100644 index 00000000..96248231 --- /dev/null +++ b/eggrecipe/src/zc/recipe/egg/__init__.py @@ -0,0 +1 @@ +from zc.recipe.egg.egg import Egg diff --git a/eggrecipe/src/zc/recipe/egg/egg.py b/eggrecipe/src/zc/recipe/egg/egg.py new file mode 100644 index 00000000..08f46039 --- /dev/null +++ b/eggrecipe/src/zc/recipe/egg/egg.py @@ -0,0 +1,50 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Install packages as eggs + +$Id$ +""" + +import zc.buildout.egglinker +import zc.buildout.easy_install + +class Egg: + + def __init__(self, buildout, name, options): + self.buildout = buildout + self.name = name + self.options = options + + def install(self): + distribution = self.options.get('distribution', self.name) + links = self.options.get( + 'find_links', + self.buildout['buildout'].get('find_links'), + ) + if links: + links = links.split() + else: + links = () + + buildout = self.buildout + zc.buildout.easy_install.install( + distribution, + buildout.eggs, + [buildout.buildout_path(link) for link in links], + always_copy = True, + ) + + zc.buildout.egglinker.scripts( + [distribution], buildout.bin, [buildout.eggs], + ) diff --git a/eggrecipe/src/zc/recipe/egg/tests.py b/eggrecipe/src/zc/recipe/egg/tests.py new file mode 100644 index 00000000..2a057613 --- /dev/null +++ b/eggrecipe/src/zc/recipe/egg/tests.py @@ -0,0 +1,100 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## + +import os, re, shutil, sys, tempfile +import pkg_resources +import zc.buildout.testing + +import unittest +from zope.testing import doctest, renormalizing + +def runsetup(d): + here = os.getcwd() + try: + os.chdir(d) + os.spawnle( + os.P_WAIT, sys.executable, sys.executable, + 'setup.py', '-q', 'bdist_egg', + {'PYTHONPATH': os.path.dirname(pkg_resources.__file__)}, + ) + shutil.rmtree('build') + finally: + os.chdir(here) + +def dirname(d, level=1): + if level == 0: + return d + return dirname(os.path.dirname(d), level-1) + +def setUp(test): + zc.buildout.testing.buildoutSetUp(test) + open(os.path.join(test.globs['sample_buildout'], + 'eggs', 'zc.recipe.egg.egg-link'), + 'w').write(dirname(__file__, 4)) + + sample = tempfile.mkdtemp('eggtest') + test.globs['_sample_eggs_container'] = sample + test.globs['sample_eggs'] = os.path.join(sample, 'dist') + zc.buildout.testing.write(sample, 'README.txt', '') + zc.buildout.testing.write(sample, 'eggrecipedemobeeded.py', 'y=1\n') + zc.buildout.testing.write( + sample, 'setup.py', + "from setuptools import setup\n" + "setup(name='demoneeded', py_modules=['eggrecipedemobeeded']," + " zip_safe=True, version='1.0')\n" + ) + runsetup(sample) + os.remove(os.path.join(sample, 'eggrecipedemobeeded.py')) + for i in (1, 2, 3): + zc.buildout.testing.write( + sample, 'eggrecipedemo.py', + 'import eggrecipedemobeeded\n' + 'x=%s\n' + 'def main(): print x, eggrecipedemobeeded.y\n' + % i) + zc.buildout.testing.write( + sample, 'setup.py', + "from setuptools import setup\n" + "setup(name='demo', py_modules=['eggrecipedemo']," + " install_requires = 'demoneeded'," + " entry_points={'console_scripts': ['demo = eggrecipedemo:main']}," + " zip_safe=True, version='0.%s')\n" % i + ) + runsetup(sample) + +def tearDown(test): + shutil.rmtree(test.globs['_sample_eggs_container']) + zc.buildout.testing.buildoutTearDown(test) + + +def test_suite(): + return unittest.TestSuite(( + #doctest.DocTestSuite(), + doctest.DocFileSuite( + 'README.txt', + setUp=setUp, tearDown=tearDown, + checker=renormalizing.RENormalizing([ + (re.compile('\S+[/%(sep)s]' + '(\\w+-)[^ \t\n%(sep)s/]+.egg' + % dict(sep=os.path.sep) + ), + '\\1-VVV-egg') + ]) + ), + + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..e3c154f8 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup, find_packages + +setup( + name = "zc.buildout", + version = "0.1", + packages = ['zc.buildout'], + package_dir = {'':'src'}, + namespace_packages = ['zc'], + include_package_data = True, + tests_require = ['zope.testing'], + test_suite = 'zc.buildout.tests.test_suite', + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "System for managing development buildouts", + license = "ZPL 2.1", + keywords = "development build", + install_requires = 'setuptools', + entry_points = {'console_scripts': ['buildout = zc.buildout.build:main']}, + ) diff --git a/src/zc/__init__.py b/src/zc/__init__.py new file mode 100644 index 00000000..35cf25b7 --- /dev/null +++ b/src/zc/__init__.py @@ -0,0 +1,5 @@ +try: + __import__('pkg_resources').declare_namespace(__name__) +except: + # bootstrapping + pass diff --git a/src/zc/buildout/__init__.py b/src/zc/buildout/__init__.py new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/src/zc/buildout/__init__.py @@ -0,0 +1 @@ +# diff --git a/src/zc/buildout/build.py b/src/zc/buildout/build.py new file mode 100644 index 00000000..fdabb653 --- /dev/null +++ b/src/zc/buildout/build.py @@ -0,0 +1,320 @@ +############################################################################## +# +# Copyright (c) 2005 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Buildout main script + +$Id$ +""" + +import md5 +import os +import pprint +import re +import shutil +import sys +import ConfigParser + +import zc.buildout.easy_install +import pkg_resources +import zc.buildout.easy_install +import zc.buildout.egglinker + +class MissingOption(KeyError): + """A required option was missing + """ + +class Options(dict): + + def __init__(self, buildout, section, data): + self.buildout = buildout + self.section = section + super(Options, self).__init__(data) + + def __getitem__(self, option): + try: + return super(Options, self).__getitem__(option) + except KeyError: + # XXX need test + raise MissingOption("Missing option", self.section, option) + + def copy(self): + return Options(self.buildout, self.section, self) + +class Buildout(dict): + + def __init__(self): + self._buildout_dir = os.path.abspath(os.getcwd()) + self._config_file = self.buildout_path('buildout.cfg') + + super(Buildout, self).__init__(self._open( + directory = self._buildout_dir, + eggs_directory = 'eggs', + bin_directory = 'bin', + parts_directory = 'parts', + installed = '.installed.cfg', + )) + + options = self['buildout'] + + links = options.get('find_links', '') + self._links = links and links.split() or () + + # XXX need tests for alternate directory locations + + for name in ('bin', 'parts', 'eggs'): + d = self.buildout_path(options[name+'_directory']) + setattr(self, name, d) + if not os.path.exists(d): + os.mkdir(d) + + _template_split = re.compile('([$]{\w+:\w+})').split + def _open(self, **predefined): + # Open configuration files + parser = ConfigParser.SafeConfigParser() + parser.add_section('buildout') + for k, v in predefined.iteritems(): + parser.set('buildout', k, v) + parser.read(self._config_file) + + data = dict([ + (section, + Options(self, section, + [(k, v.strip()) for (k, v) in parser.items(section)]) + ) + for section in parser.sections() + ]) + + converted = {} + for section, options in data.iteritems(): + for option, value in options.iteritems(): + if '$' in value: + value = self._dosubs(section, option, value, + data, converted, []) + options[option] = value + converted[(section, option)] = value + + # XXX need various error tests + + return data + + def _dosubs(self, section, option, value, data, converted, seen): + key = section, option + r = converted.get(key) + if r is not None: + return r + if key in seen: + raise ValueError('Circular references', seen, key) + seen.append(key) + value = '$$'.join([self._dosubs_esc(s, data, converted, seen) + for s in value.split('$$') + ]) + seen.pop() + return value + + def _dosubs_esc(self, value, data, converted, seen): + value = self._template_split(value) + subs = [] + for s in value[1::2]: + s = tuple(s[2:-1].split(':')) + v = converted.get(s) + if v is None: + options = data.get(s[0]) + if options is None: + raise KeyError("Referenced section does not exist", s[0]) + v = options.get(s[1]) + if v is None: + raise KeyError("Referenced option does not exist", *s) + if '$' in v: + v = _dosubs(s[0], s[1], v, data, converted, seen) + options[s[1]] = v + converted[s] = v + subs.append(v) + subs.append('') + + return ''.join([''.join(v) for v in zip(value[::2], subs)]) + + # XXX test + def buildout_path(self, *names): + return os.path.join(self._buildout_dir, *names) + + # XXX test + def distributions_path(self, specs): + return zc.buildout.egglinker.path(specs, [self.eggs]) + + def install(self): + self._develop() + new_part_options = self._gather_part_info() + installed_part_options = self._read_installed_part_options() + old_parts = installed_part_options['buildout']['parts'].split() + old_parts.reverse() + + new_old_parts = [] + for part in old_parts: + installed_options = installed_part_options[part].copy() + installed = installed_options.pop('__buildout_installed__') + if installed_options != new_part_options.get(part): + self._uninstall(installed) + del installed_part_options[part] + else: + new_old_parts.append(part) + new_old_parts.reverse() + + new_parts = [] + try: + for part in new_part_options['buildout']['parts'].split(): + installed = self._install(part) + new_part_options[part]['__buildout_installed__'] = installed + new_parts.append(part) + installed_part_options[part] = new_part_options[part] + new_old_parts = [p for p in new_old_parts if p != part] + finally: + new_parts.extend(new_old_parts) + installed_part_options['buildout']['parts'] = ' '.join(new_parts) + self._save_installed_options(installed_part_options) + + def _develop(self): + """Install sources by running setup.py develop on them + """ + develop = self['buildout'].get('develop') + if develop: + here = os.getcwd() + try: + for setup in develop.split(): + setup = self.buildout_path(setup) + if os.path.isdir(setup): + setup = os.path.join(setup, 'setup.py') + + os.chdir(os.path.dirname(setup)) + os.spawnle( + os.P_WAIT, sys.executable, sys.executable, + setup, '-q', 'develop', '-m', '-x', + '-f', ' '.join(self._links), + '-d', self.eggs, + {'PYTHONPATH': + os.path.dirname(pkg_resources.__file__)}, + ) + finally: + os.chdir(os.path.dirname(here)) + + def _gather_part_info(self): + """Get current part info, including part options and recipe info + """ + parts = self['buildout']['parts'] + part_info = {'buildout': {'parts': parts}} + recipes_requirements = [] + pkg_resources.working_set.add_entry(self.eggs) + + parts = parts and parts.split() or [] + for part in parts: + options = self.get(part) + if options is None: + options = self[part] = {} + options = options.copy() + recipe, entry = self._recipe(part, options) + zc.buildout.easy_install.install( + recipe, self.eggs, self._links) + recipes_requirements.append(recipe) + part_info[part] = options + + # Load up the recipe distros + pkg_resources.require(recipes_requirements) + + base = self.eggs + os.path.sep + for part in parts: + options = part_info[part] + recipe, entry = self._recipe(part, options) + req = pkg_resources.Requirement.parse(recipe) + sig = _dists_sig(pkg_resources.working_set.resolve([req]), base) + options['__buildout_signature__'] = ' '.join(sig) + + return part_info + + def _recipe(self, part, options): + recipe = options.get('recipe', part) + if ':' in recipe: + recipe, entry = recipe.split(':') + else: + entry = 'default' + + return recipe, entry + + def _read_installed_part_options(self): + old = self._installed_path() + if os.path.isfile(old): + parser = ConfigParser.SafeConfigParser() + parser.read(old) + return dict([(section, dict(parser.items(section))) + for section in parser.sections()]) + else: + return {'buildout': {'parts': ''}} + + def _installed_path(self): + return self.buildout_path(self['buildout']['installed']) + + def _uninstall(self, installed): + for f in installed.split(): + f = self.buildout_path(f) + if os.path.isdir(f): + shutil.rmtree(f) + elif os.path.isfile(f): + os.remove(f) + + def _install(self, part): + options = self[part] + recipe, entry = self._recipe(part, options) + recipe_class = pkg_resources.load_entry_point( + recipe, 'zc.buildout', entry) + installed = recipe_class(self, part, options).install() + if installed is None: + installed = [] + elif isinstance(installed, basestring): + installed = [installed] + base = self.buildout_path('') + installed = [d.startswith(base) and d[len(base):] or d + for d in installed] + return ' '.join(installed) + + def _save_installed_options(self, installed_options): + parser = ConfigParser.SafeConfigParser() + for section in installed_options: + parser.add_section(section) + for option, value in installed_options[section].iteritems(): + parser.set(section, option, value) + parser.write(open(self._installed_path(), 'w')) + +def _dir_hash(dir): + hash = md5.new() + for (dirpath, dirnames, filenames) in os.walk(dir): + filenames[:] = [f for f in filenames + if not (f.endswith('pyc') or f.endswith('pyo')) + ] + hash.update(' '.join(dirnames)) + hash.update(' '.join(filenames)) + for name in filenames: + hash.update(open(os.path.join(dirpath, name)).read()) + return hash.digest().encode('base64').strip() + +def _dists_sig(dists, base): + result = [] + for dist in dists: + location = dist.location + if dist.precedence == pkg_resources.DEVELOP_DIST: + result.append(dist.project_name + '-' + _dir_hash(location)) + else: + if location.startswith(base): + location = location[len(base):] + result.append(location) + return result + +def main(): + Buildout().install() diff --git a/src/zc/buildout/buildout.txt b/src/zc/buildout/buildout.txt new file mode 100644 index 00000000..19610a77 --- /dev/null +++ b/src/zc/buildout/buildout.txt @@ -0,0 +1,363 @@ +Defining Buildouts +================== + +This document describes how to define buildouts using buildout +configuation files and recipes. It doesn't describe how to bootstrap +a buildout. To find out how to do that, see bootstrap.txt. For the +examples we show here, we've created a sample buildout that already +contains the mimimal software needed for a buildout. + +Buildouts are defined using configuration files. These are in the +format defined by the Python ConfigParser module, with an extension +that we'll describe later. When a buildout is run, it looks for +the file buildout.cfg in the directory where the buidout is run. It +will optionally look for buildout-instance.cfg. Typically, buidout.cfg +contains information common to all instances of a buildout and is +checked in, and buildout-instance.cfg has instance-specific information. + +We have a sample buildout that has already been created for us. It +has the absolute minimum information. We have bin, eggs and parts +directories, and a configuration file: + + >>> ls(sample_buildout) + - .installed.cfg + d bin + - buildout.cfg + d eggs + d parts + +The bin directory contains scripts. A minimal buildout has a build +script and a py_zc.buildout script: + + >>> ls(sample_buildout, 'bin') + - buildout + +The build script is what we run to build things out. The +py_zc.buildout script gives us a Python prompt with the Python path +set to that needed by the zc.buildout package. + +The eggs directory is initially empty. This is typically the case +when the zc.buildout and setuptools are installed externally to the +buildout: + + >>> ls(sample_buildout, 'eggs') + +They can also be installed locally in a buildout, in which case they's +show up as eggs in the eggs directory. + +The parts directory is initially empty: + + >>> ls(sample_buildout, 'parts') + +The parts directory provides an area where recipies can install +part data. For example, if we built a custom Python, we would +install it in the part directory. Part data is stored in a +subdirectory of the parts directory with the same name as the part. + +The file .installed.cfg contains information about previously installed +parts. Because this is a new buildout, this file isn't very +interesting: + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = + + +The minimal configuration file has a buildout section that defines no +parts: + + >>> cat(sample_buildout, 'buildout.cfg') + [buildout] + parts = + +A part is simply something to be created by a buildout. It can be +almost anything, such as a Python package, a program, a directory, or +a confguration file. + +A part is created by a recipe. Recipes are always installed as Python +eggs. They can be downloaded from an package server, such as the +Python package index, or they can be developed as part of a project. +Let's create a recipe as part of the sample project. We'll create a +recipe for creating directories. + +First, we'll create a recipes directory for +our local recipes: + + >>> mkdir(sample_buildout, 'recipes') + +and then we'll create a source file for our mkdir recipe: + + >>> write(sample_buildout, 'recipes', 'mkdir.py', + ... """ + ... import os + ... + ... class Mkdir: + ... + ... def __init__(self, buildout, name, options): + ... self.buildout = buildout + ... self.name = name + ... self.options = options + ... + ... def install(self): + ... path = self.buildout.buildout_path(self.options['path']) + ... if not os.path.isdir(path): + ... print 'Creating directory', os.path.basename(path) + ... os.mkdir(path) + ... return path + ... """) + +The recipe defines a constructor that takes a buildout object, a part +name, and an options dictionary. It saves them in instance attributes. + +The install method is responsible for creating the part. In this +case, we need the path of the directory to create. We'll use a +buildout option from our options dictionary. If the path is relative, +we'll interpret it relative to the buildout directory. The buildout +buildout_path method gives us a path relative to the buildout. It +uses os.path.join, so if we pass it an absolute path, we'll get the +absolute path back. (If no arguments are passed to base_path, then the +buildout directory is returned.) + +We made the method chatty so that we can observe what it's doing. + +We return the path that we installed. If the part is unistalled or +reinstalled, then the path returned will be removed by the buildout +machinery. A recipe install method is expected to return None, a +string, or an iterable of strings containing paths to be removed if a +part is uninstalled. + +We need to provide packaging information so that our recipe can be +installed as an egg. We need to define a setup script for this: + + >>> write(sample_buildout, 'recipes', 'setup.py', + ... """ + ... from setuptools import setup + ... + ... setup( + ... name = "recipes", + ... entry_points = {'zc.buildout': ['mkdir = mkdir:Mkdir']}, + ... ) + ... """) + +Here we've defined a package containing just our module. We've also +defined an entry point. Entry points provide a way for an egg to +define the services it provides. Here we've said that we define a +zc.buildout entry point named default. Recipe classes must be exposed +as entry points in the zc.buildout group. we give entry points names +within the group. The name "default" is somewhat special because it +allows a recipe to be referenced using a package name without naming +an entry point. + +We also need a README.txt for our recipes to avoid a warning: + + >>> write(sample_buildout, 'recipes', 'README.txt', " ") + +Now let's update our buildout.cfg: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = data_dir + ... + ... [data_dir] + ... recipe = recipes:mkdir + ... path = mystuff + ... """) + +Let's go through the changes one by one:: + + develop = recipes + +This tells the buildout to install a development egg for our recipes. +Any number of paths can be listed. The paths can be relative or +absolute. If relative, they are treated as relative to the buidlout +directory. They can be directory or file paths. If a file path is +given, it should point to a Python setup script. If a directory path +is given, it should point to a directory containing a setup.py. +Development eggs are installed before building any parts, as they may +provide locally-defined recipes needed by the parts. + +:: + + parts = data_dir + +Here we've named a part to be "built". We can use any name we want +except that different part names must be unique and recipes will often +use the part name to decide what to do. + +:: + + [data_dir] + recipe = recipes:mkdir + path = mystuff + +Generally, when we name a part, we also create a section of the same +name that contains part data. In this section, we'll usually define +the recipe to be used to install the part. In this case, we also +specify the path to be created. + +Let's run the buildout. We do so by running the build script in the +buildout: + + >>> import os + >>> os.chdir(sample_buildout) + >>> runscript = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(runscript), + Creating directory mystuff + +We see that the recipe created the directory, as expected: + + >>> ls(sample_buildout) + - .installed.cfg + d bin + - buildout.cfg + d eggs + d mystuff + d parts + d recipes + +In addition, .installed.cfg has been updated to reflect the part we +installed: + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = data_dir + <BLANKLINE> + [data_dir] + path = mystuff + recipe = recipes:mkdir + __buildout_signature__ = recipes-O3ypTgKOkHMqMwKvMfvHnA== + __buildout_installed__ = mystuff + <BLANKLINE> + +Note that the directory we installed is included in .installed.cfg. + +If we change the name of the directory in the configuration file, +we'll see that the directory gets removed and recreated: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = data_dir + ... + ... [data_dir] + ... recipe = recipes:mkdir + ... path = mydata + ... """) + + >>> print system(runscript), + Creating directory mydata + + >>> ls(sample_buildout) + - .installed.cfg + d bin + - buildout.cfg + d eggs + d mydata + d parts + d recipes + +Variable substitutions +---------------------- + +Buildout configuration files support two kinds of substitutions, +standard ConfigParser substitutions, and string-template +substitutions. To illustrate this, we'll create an debug recipe to +allow us to see interactions with the buildout: + + >>> write(sample_buildout, 'recipes', 'debug.py', + ... """ + ... class Debug: + ... + ... def __init__(self, buildout, name, options): + ... self.buildout = buildout + ... self.name = name + ... self.options = options + ... + ... def install(self): + ... items = self.options.items() + ... items.sort() + ... for option, value in items: + ... print option, value + ... """) + +In this example, we've used a simple base class that provides a +boilerplate constructor. This recipe doesn't actually create +anything. The install method doesn't return anything, because it +didn't create any files or directories. + +We also have to update our setup script: + + >>> write(sample_buildout, 'recipes', 'setup.py', + ... """ + ... from setuptools import setup + ... entry_points = ( + ... ''' + ... [zc.buildout] + ... mkdir = mkdir:Mkdir + ... debug = debug:Debug + ... ''') + ... setup(name="recipes", entry_points=entry_points) + ... """) + +We've rearranged the script a bit to make the entry points easier to +edit. + +Let's update our configuration to provide variable substitution +examples: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... + ... [debug] + ... recipe = recipes:debug + ... file1 = ${data_dir:path}/file + ... file2 = %(file1)s.out + ... file3 = %(base)s/file3 + ... + ... [data_dir] + ... recipe = recipes:mkdir + ... path = mydata + ... + ... [DEFAULT] + ... base = var + ... """) + +In this example, we've used ConfigParser substitutions for file2 and +file3. This type of substitution uses Python string format syntax. +Valid names are option in the same section and options defined in the +DEFAULT section. We used a string-template substitution for file1. +This type of substituion uses the string.Template syntax. Names +substited are qualified option names, consisting of a section name and +option name joined by a colon. + +Now, if we run the buildout, we'll see the options with the values +substituted. + + >>> print system(runscript), + Creating directory mydata + base var + file1 mydata/file + file2 mydata/file.out + file3 var/file3 + recipe recipes:debug + +It might seem surprising that mydata was created again. This is +because we changed our recipes package by adding the debug module. +The buildout system didn't know if this module could effect the mkdir +recipe, so it assumed it could and reinstalled mydata. If we rerun +the buildout: + + >>> print system(runscript), + base var + file1 mydata/file + file2 mydata/file.out + file3 var/file3 + recipe recipes:debug + +We can see that mydata was not recreated. diff --git a/src/zc/buildout/easy_install.py b/src/zc/buildout/easy_install.py new file mode 100644 index 00000000..4ea68a91 --- /dev/null +++ b/src/zc/buildout/easy_install.py @@ -0,0 +1,54 @@ +############################################################################## +# +# Copyright (c) 2005 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Python easy_install API + +This module provides a high-level Python API for installing packages. +It doesn't install scripts. It uses setuptools and requires it to be +installed. + +$Id$ +""" + +# XXX needs doctest + +import setuptools.command.easy_install +import pkg_resources +import setuptools.package_index +import distutils.dist +import distutils.log + +def install(spec, dest, links=(), **kw): + index = setuptools.package_index.PackageIndex() + index.add_find_links(links) + easy = setuptools.command.easy_install.easy_install( + distutils.dist.Distribution(), + multi_version=True, + exclude_scripts=True, + sitepy_installed=True, + install_dir=dest, + outputs=[], + verbose = 0, + args = [spec], + find_links = links, + **kw + ) + easy.finalize_options() + + old_warn = distutils.log.warn + distutils.log.warn = lambda *a, **k: None + + easy.easy_install(spec, deps=True) + + distutils.log.warn = old_warn + diff --git a/src/zc/buildout/egglinker.py b/src/zc/buildout/egglinker.py new file mode 100644 index 00000000..14d08c3a --- /dev/null +++ b/src/zc/buildout/egglinker.py @@ -0,0 +1,111 @@ +############################################################################## +# +# Copyright (c) 2005 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Egg linker -- Link eggs together to build applications + +Egg linker is a script that generates startup scripts for eggs that +include an egg's working script in the generated script. + +The egg linker module also exports helper functions of varous kinds to +assist in custom script generation. + +$Id$ +""" + +# XXX needs doctest + +import os +import sys + +import pkg_resources + +def distributions(reqs, eggss): + env = pkg_resources.Environment(eggss) + ws = pkg_resources.WorkingSet() + reqs = [pkg_resources.Requirement.parse(r) for r in reqs] + reqs.append(pkg_resources.Requirement.parse('setuptools')) + return ws.resolve(reqs, env=env) + +def path(reqs, eggss): + dists = distributions(reqs, eggss) + return [dist.location for dist in dists] + +def location(spec, eggss): + env = pkg_resources.Environment(eggss) + req = pkg_resources.Requirement.parse(spec) + dist = env.best_match(req, pkg_resources.WorkingSet()) + return dist.location + +def scripts(reqs, dest, eggss): + dists = distributions(reqs, eggss) + reqs = [pkg_resources.Requirement.parse(r) for r in reqs] + projects = [r.project_name for r in reqs] + path = "',\n '".join([dist.location for dist in dists]) + + for dist in dists: + if dist.project_name in projects: + for name in pkg_resources.get_entry_map(dist, 'console_scripts'): + _script(dist, name, path, os.path.join(dest, name)) + _pyscript(path, os.path.join(dest, 'py_'+dist.project_name)) + +def _script(dist, name, path, dest): + open(dest, 'w').write(script_template % dict( + python = sys.executable, + path = path, + project = dist.project_name, + name = name, + )) + try: + os.chmod(dest, 0755) + except (AttributeError, os.error): + pass + +script_template = '''\ +#!%(python)s + +import sys +sys.path[0:0] = [ + '%(path)s' + ] + +from pkg_resources import load_entry_point + +sys.exit( + load_entry_point('%(project)s', 'console_scripts', '%(name)s')() +) + +''' + + +def _pyscript(path, dest): + open(dest, 'w').write(py_script_template % dict( + python = sys.executable, + path = path, + )) + try: + os.chmod(dest,0755) + except (AttributeError, os.error): + pass + +py_script_template = '''\ +#!%(python)s -i + +import sys +sys.path[0:0] = [ + '%(path)s' + ] +''' + +def main(): + import pdb; pdb.set_trace() + diff --git a/src/zc/buildout/testing.py b/src/zc/buildout/testing.py new file mode 100644 index 00000000..30c36b8b --- /dev/null +++ b/src/zc/buildout/testing.py @@ -0,0 +1,109 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""XXX short summary goes here. + +$Id$ +""" + +import os, re, shutil, sys, tempfile, unittest +from zope.testing import doctest, renormalizing + + +def cat(dir, *names): + path = os.path.join(dir, *names) + print open(path).read(), + +def ls(dir, *subs): + if subs: + dir = os.path.join(dir, *subs) + names = os.listdir(dir) + names.sort() + for name in names: + if os.path.isdir(os.path.join(dir, name)): + print 'd ', + else: + print '- ', + print name + +def mkdir(dir, *subs): + if subs: + dir = os.path.join(dir, *subs) + os.mkdir(dir) + +def write(dir, *args): + open(os.path.join(dir, *(args[:-1])), 'w').write(args[-1]) + +def system(command, input=''): + i, o = os.popen4(command) + if input: + i.write(input) + i.close() + return o.read() + +def dirname(path, n=1): + if n <= 0: + return path + return dirname(os.path.dirname(path), n-1) + +def buildoutSetUp(test): + sample = tempfile.mkdtemp('buildout-tests') + for name in ('bin', 'eggs', 'parts'): + os.mkdir(os.path.join(sample, name)) + + # make sure we can import zc.buildout and setuptools + import zc.buildout, setuptools + + # Generate buildout script + dest = os.path.join(sample, 'bin', 'buildout') + open(dest, 'w').write( + script_template % dict(python=sys.executable, path=sys.path) + ) + try: + os.chmod(dest, 0755) + except (AttributeError, os.error): + pass + + + open(os.path.join(sample, 'buildout.cfg'), 'w').write( + "[buildout]\nparts =\n" + ) + open(os.path.join(sample, '.installed.cfg'), 'w').write( + "[buildout]\nparts =\n" + ) + + test.globs.update(dict( + __here = os.getcwd(), + sample_buildout = sample, + ls = ls, + cat = cat, + mkdir = mkdir, + write = write, + system = system, + __original_wd__ = os.getcwd(), + )) + +def buildoutTearDown(test): + shutil.rmtree(test.globs['sample_buildout']) + os.chdir(test.globs['__original_wd__']) + + +script_template = '''\ +#!%(python)s + +import sys +sys.path[0:0] = %(path)r + +from pkg_resources import load_entry_point +sys.exit(load_entry_point('zc.buildout', 'console_scripts', 'buildout')()) +''' diff --git a/src/zc/buildout/tests.py b/src/zc/buildout/tests.py new file mode 100644 index 00000000..799991a3 --- /dev/null +++ b/src/zc/buildout/tests.py @@ -0,0 +1,35 @@ +############################################################################## +# +# Copyright (c) 2004 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""XXX short summary goes here. + +$Id$ +""" + +import unittest +from zope.testing import doctest + +from zc.buildout.testing import buildoutSetUp, buildoutTearDown + +def test_suite(): + return unittest.TestSuite(( + #doctest.DocTestSuite(), + doctest.DocFileSuite( + 'buildout.txt', + setUp=buildoutSetUp, tearDown=buildoutTearDown, + ), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff --git a/testrunnerrecipe/README.txt b/testrunnerrecipe/README.txt new file mode 100644 index 00000000..0fda7401 --- /dev/null +++ b/testrunnerrecipe/README.txt @@ -0,0 +1 @@ +Recipe for generating a custom test runner. diff --git a/testrunnerrecipe/setup.py b/testrunnerrecipe/setup.py new file mode 100644 index 00000000..8238d258 --- /dev/null +++ b/testrunnerrecipe/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + +setup( + name = "zc.recipe.testrunner", + version = "0.1", + packages = find_packages('src'), + include_package_data = True, + package_dir = {'':'src'}, + namespace_packages = ['zc', 'zc.recipe'], + install_requires = ['zc.buildout', 'zope.testing'], + dependency_links = ['http://download.zope.org/distribution/'], + test_suite = 'zc.recipe.testrunner.tests.test_suite', + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "ZC Buildout recipe for creating test runners", + license = "ZPL 2.1", + keywords = "development build", + entry_points = {'zc.buildout': + ['default = zc.recipe.testrunner:TestRunner']}, + ) diff --git a/testrunnerrecipe/src/zc/__init__.py b/testrunnerrecipe/src/zc/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/testrunnerrecipe/src/zc/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/testrunnerrecipe/src/zc/recipe/__init__.py b/testrunnerrecipe/src/zc/recipe/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/testrunnerrecipe/src/zc/recipe/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/testrunnerrecipe/src/zc/recipe/testrunner.py b/testrunnerrecipe/src/zc/recipe/testrunner.py new file mode 100644 index 00000000..85f10aff --- /dev/null +++ b/testrunnerrecipe/src/zc/recipe/testrunner.py @@ -0,0 +1,63 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""A few built-in recipes + +$Id$ +""" + +# XXX need tests + +import os, sys + +class TestRunner: + + def __init__(self, buildout, name, options): + self.buildout = buildout + self.name = name + self.options = options + + def install(self): + distributions = self.options['distributions'].split() + path = self.buildout.distributions_path(distributions+['zope.testing']) + locations = [self.buildout.distribution_location(distribution) + for distribution in distributions] + script = self.options.get('script', self.name) + script = self.buildout.buildout_path('bin', script) + open(script, 'w').write(tests_template % dict( + PYTHON=sys.executable, + PATH="',\n '".join(path), + TESTPATH="',\n '--test-path', '".join(locations), + )) + try: + os.chmod(script, 0755) + except (AttributeError, os.error): + pass + + +tests_template = """#!%(PYTHON)s + +import sys +sys.path[0:0] = [ + '%(PATH)s', + ] + +from zope.testing import testrunner + +defaults = [ + '--test-path', '%(TESTPATH)s', + ] + +sys.exit(testrunner.run(defaults)) +""" + diff --git a/todo.txt b/todo.txt new file mode 100644 index 00000000..495d7946 --- /dev/null +++ b/todo.txt @@ -0,0 +1,40 @@ +- Missing tests. See XXXs + +- Buildout command-line options: + + --config -C specify a config files + + --option -O specify options + +- Python discovery support + + (Perhaps this is best handled by DEFAULT section. + +- Common recipes + + - configure-make-make-install + + - download, checkout + + - Should ot be possible to provide multiple recipies? + Or should recipies be combined through inheritence (or + composition)? + + - Python + +- Need to better understand the way upgrading works in setuptools. + +- Multiple setupfiles, + + o extends, optionally-extends, extended-by, optionally-exteded-by + + o instance.ini + + o ~/buildout/default.ini + +- Offline mode + +- Local download cache + +- Logging + -- 2.30.9