From 7daada49d7ec66f8ace4f350217514af8b695884 Mon Sep 17 00:00:00 2001 From: jim <jim@62d5b8a3-27da-0310-9561-8e5933582275> Date: Tue, 29 Aug 2006 17:46:21 +0000 Subject: [PATCH] Factor out setuptools support from the buildout git-svn-id: http://svn.zope.org/repos/main/zc.buildout/trunk@69872 62d5b8a3-27da-0310-9561-8e5933582275 --- zc.setuptools/README.txt | 233 ++++ zc.setuptools/bootstrap/bootstrap.py | 52 + zc.setuptools/buildout.cfg | 13 + zc.setuptools/dev.py | 44 + zc.setuptools/setup.py | 26 + zc.setuptools/specifications/README.txt | 4 + zc.setuptools/specifications/repeatable.txt | 100 ++ zc.setuptools/src/zc/__init__.py | 5 + zc.setuptools/src/zc/buildout/__init__.py | 1 + zc.setuptools/src/zc/buildout/buildout.py | 711 ++++++++++ zc.setuptools/src/zc/buildout/buildout.txt | 1201 +++++++++++++++++ zc.setuptools/src/zc/buildout/easy_install.py | 461 +++++++ .../src/zc/buildout/easy_install.txt | 440 ++++++ zc.setuptools/src/zc/buildout/testing.py | 418 ++++++ zc.setuptools/src/zc/buildout/tests.py | 343 +++++ zc.setuptools/todo.txt | 66 + zc.setuptools/zc.recipe.egg_/README.txt | 69 + zc.setuptools/zc.recipe.egg_/setup.py | 27 + .../zc.recipe.egg_/src/zc/__init__.py | 1 + .../zc.recipe.egg_/src/zc/recipe/__init__.py | 1 + .../src/zc/recipe/egg/README.txt | 269 ++++ .../src/zc/recipe/egg/__init__.py | 2 + .../zc.recipe.egg_/src/zc/recipe/egg/api.txt | 110 ++ .../src/zc/recipe/egg/custom.py | 82 ++ .../src/zc/recipe/egg/custom.txt | 114 ++ .../zc.recipe.egg_/src/zc/recipe/egg/egg.py | 107 ++ .../src/zc/recipe/egg/selecting-python.txt | 208 +++ .../zc.recipe.egg_/src/zc/recipe/egg/tests.py | 135 ++ zc.setuptools/zc.recipe.testrunner/README.txt | 26 + zc.setuptools/zc.recipe.testrunner/setup.py | 25 + .../zc.recipe.testrunner/src/zc/__init__.py | 1 + .../src/zc/recipe/__init__.py | 1 + .../src/zc/recipe/testrunner/README.txt | 174 +++ .../src/zc/recipe/testrunner/__init__.py | 89 ++ .../src/zc/recipe/testrunner/tests.py | 62 + 35 files changed, 5621 insertions(+) create mode 100644 zc.setuptools/README.txt create mode 100644 zc.setuptools/bootstrap/bootstrap.py create mode 100644 zc.setuptools/buildout.cfg create mode 100644 zc.setuptools/dev.py create mode 100644 zc.setuptools/setup.py create mode 100644 zc.setuptools/specifications/README.txt create mode 100644 zc.setuptools/specifications/repeatable.txt create mode 100644 zc.setuptools/src/zc/__init__.py create mode 100644 zc.setuptools/src/zc/buildout/__init__.py create mode 100644 zc.setuptools/src/zc/buildout/buildout.py create mode 100644 zc.setuptools/src/zc/buildout/buildout.txt create mode 100644 zc.setuptools/src/zc/buildout/easy_install.py create mode 100644 zc.setuptools/src/zc/buildout/easy_install.txt create mode 100644 zc.setuptools/src/zc/buildout/testing.py create mode 100644 zc.setuptools/src/zc/buildout/tests.py create mode 100644 zc.setuptools/todo.txt create mode 100644 zc.setuptools/zc.recipe.egg_/README.txt create mode 100644 zc.setuptools/zc.recipe.egg_/setup.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/__init__.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/__init__.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/README.txt create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/__init__.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/api.txt create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.txt create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/egg.py create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/selecting-python.txt create mode 100644 zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/tests.py create mode 100644 zc.setuptools/zc.recipe.testrunner/README.txt create mode 100644 zc.setuptools/zc.recipe.testrunner/setup.py create mode 100644 zc.setuptools/zc.recipe.testrunner/src/zc/__init__.py create mode 100644 zc.setuptools/zc.recipe.testrunner/src/zc/recipe/__init__.py create mode 100644 zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/README.txt create mode 100644 zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/__init__.py create mode 100644 zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/tests.py diff --git a/zc.setuptools/README.txt b/zc.setuptools/README.txt new file mode 100644 index 00000000..aca78522 --- /dev/null +++ b/zc.setuptools/README.txt @@ -0,0 +1,233 @@ +============= +Zope Buildout +============= + +.. contents:: + +The Zope Buildout project provides support for creating applications, +especially Python applications. It provides tools for assembling +applications from multiple parts, Python or otherwise. An application +may actually contain multiple programs, processes, and configuration +settings. + +Here's an example of such an application that we built with an earlier +prototype of the buildout system. We have a Zope application consisting of: + +- Multiple Zope instances + +- 4 ZEO servers + +- An LDAP server + +- Cache-invalidation and Mail delivery servers + +- Dozens of add-on packages + +- Multiple test runners + +- Multiple deployment modes, including dev, stage, and prod, + with prod deployment over multiple servers + +Parts installed include: + +- Application software installs, including Zope, ZEO and LDAP + software + +- Add-on packages + +- Bundles of configuration that define Zope, ZEO and LDAP instances + +- Utility scripts such as test runners, server-control + scripts, cron jobs. + +This is all defined using configuration files and recipes, which are +software that build and installs parts based on configuration data. +The prototype system has minimal documentation and no tests and +has no egg support. (It build on earlier make-based systems that had +no documentation or tests.) + +This project provides a non-prototype implementation of the ideas and +knowledge gained from earlier efforts and leverages setuptools to make +recipe management cleaner and to provide better Python package and +script management. + +The word "buildout" refers to a description of a set of parts and the +software to create and 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 programs 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. + +Installation +============ + +There are two ways to install zc,buildout + +1. Install it as an egg using `easy_install + <http://peak.telecommunity.com/DevCenter/EasyInstall>`_ into a + Python instaallation. Then just use the buildout script from your + Python bin or Scripts directory. + +2. Use the `bootstrap script + <http://dev.zope.org/Buildout/bootstrap.py>`_ to install setuptools + and the buildout software into your buildout. Typically, you'll + check the bootstrap script into your project so that, whenever you + checkout your project, you can turn it into a buildout by just + running the bootstrap script. + +More information +================ + +The detailed documentation for the various parts of buildout can be +found in the following files: + +`buildout.txt <http://dev.zope.org/Buildout/buildout.html>`_ + Describes how to define and run buildouts. It also describes how + to write recipes. + +`easy_install.txt <http://dev.zope.org/Buildout/easy_install.html>`_ + Describes an Python APIs for invoking easy_install for generation + of scripts with paths baked into them. + + +Download +======== + +You can download zc.buildout and many buildout recipes from the +`Python Package Index <http://www.python.org/pypi>`_. + +Recipes +======= + +Existing recipes include: + +`zc.recipe.egg <http://dev.zope.org/Buildout/egg.html>`_ + The egg recipe installes one or more eggs, with their + dependencies. It installs their console-script entry points with + the needed eggs included in their paths. + +`zc.recipe.testrunner <http://dev.zope.org/Buildout/testrunner.html>`_ + The testrunner egg installs creates a test runner script for one or + more eggs. + +`zc.recipe.zope3checkout <http://dev.zope.org/Buildout/zope3checkout.html>`_ + The zope3checkout recipe installs a Zope 3 checkout into a + buildout. + +`zc.recipe.zope3instance <http://dev.zope.org/Buildout/zope3instance.html>`_ + The zope3instance recipe sets up a Zope 3 instance. + +`zc.recipe.filestorage <http://dev.zope.org/Buildout/filestorage.html>`_ + The filestorage recipe sets up a ZODB file storage for use in a + Zope 3 instance creayed by the zope3instance recipe. + +Buildout examples +================= + +Some simple buildout examples: + +`The zc.buildout project <http://svn.zope.org/zc.buildout/trunk>`_ + This is the project for the buildout software itself, which is + developed as a buildout. + +`The zc sharing project <http://svn.zope.org/zc.sharing/trunk>`_ + This project illistrates using the buildout software with Zope 3. + Note that the bootstrap.py file is checked in so that a buildout + can be made when the project is checked out. The buildout.cfg + specified everything needed to create a Zope 3 installation with + the zc.sharing package installed in development mode. + +Status +====== + +The buildout system is under active development. Some near term +priorities include: + +- Better error reporing + +- Windows support + +- Handling of egg extras + +- More recipes + +Questions +========= + +You can send questions to jim@zope.com. + +Change History +============== + +1.0.0b3 +------- + +- Added Windows support. + +- Fixed some bugs in variable substitutions. + + The characters "-", "." and " ", weren't allowed in section or + option names. + + Substitutions with invalid names were ignored, which caused + missleading failures downstream. + +- Improved error handling. No longer show tracebacks for user errors. + +- Now require a recipe option (and therefore a section) for every part. + +1.0.0b2 +------- + +Added support for specifying some build_ext options when installing eggs +from source distributions. + +1.0.0b1 +------- + +- Changed the bootstrapping code to only install setuptools and + zc.buildout. The bootstrap code no-longer runs the buildout itself. + This was to fix a bug that caused parts to be recreated + unnecessarily because the recipe signature in the initial buildout + reflected temporary locations for setuptools and zc.buildout. + +- Now create a minimal setup.py if it doesn't exist and issue a + warning that it is being created. + +- Fixed bug in saving installed configuration data. %'s and extra + spaces weren't quoted. + +1.0.0a1 +------- + +Initial public version diff --git a/zc.setuptools/bootstrap/bootstrap.py b/zc.setuptools/bootstrap/bootstrap.py new file mode 100644 index 00000000..89548e99 --- /dev/null +++ b/zc.setuptools/bootstrap/bootstrap.py @@ -0,0 +1,52 @@ +############################################################################## +# +# 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. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. + +$Id$ +""" + +import os, shutil, sys, tempfile, urllib2 + +tmpeggs = tempfile.mkdtemp() + +ez = {} +exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py' + ).read() in ez +ez['use_setuptools'](to_dir=tmpeggs, download_delay=0) + +import pkg_resources + +cmd = 'from setuptools.command.easy_install import main; main()' +if sys.platform == 'win32': + cmd = '"%s"' % cmd # work around spawn lamosity on windows + +ws = pkg_resources.working_set +assert os.spawnle( + os.P_WAIT, sys.executable, sys.executable, + '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout', + dict(os.environ, + 'PYTHONPATH'= + ws.find(pkg_resources.Requirement.parse('setuptools')).location + ), + ) == 0 + +ws.add_entry(tmpeggs) +ws.require('zc.buildout') +import zc.buildout.buildout +zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap']) +shutil.rmtree(tmpeggs) diff --git a/zc.setuptools/buildout.cfg b/zc.setuptools/buildout.cfg new file mode 100644 index 00000000..c6484813 --- /dev/null +++ b/zc.setuptools/buildout.cfg @@ -0,0 +1,13 @@ +[buildout] +develop = zc.recipe.egg_ zc.recipe.testrunner zc.buildoutsupport +parts = test + +# prevent slow access to cheeseshop: +index = http://download.zope.org + +[test] +recipe = zc.recipe.testrunner +eggs = + zc.buildout + zc.recipe.egg + zc.recipe.testrunner diff --git a/zc.setuptools/dev.py b/zc.setuptools/dev.py new file mode 100644 index 00000000..e6ffbab5 --- /dev/null +++ b/zc.setuptools/dev.py @@ -0,0 +1,44 @@ +############################################################################## +# +# 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 the buildout project itself. + +This is different from a normal boostrapping process because the +buildout egg itself is installed as a develop egg. + +$Id$ +""" + +import os, sys, urllib2 + +for d in 'eggs', 'develop-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 pkg_resources + +os.spawnle(os.P_WAIT, sys.executable, sys.executable, 'setup.py', + '-q', 'develop', '-m', '-x', '-d', 'develop-eggs', + {'PYTHONPATH': os.path.dirname(pkg_resources.__file__)}, + ) +pkg_resources.working_set.add_entry('src') + +import zc.buildout.easy_install +zc.buildout.easy_install.scripts( + ['zc.buildout'], pkg_resources.working_set , sys.executable, 'bin') +sys.exit(os.spawnl(os.P_WAIT, 'bin/buildout', 'bin/buildout')) diff --git a/zc.setuptools/setup.py b/zc.setuptools/setup.py new file mode 100644 index 00000000..242212dd --- /dev/null +++ b/zc.setuptools/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup, find_packages + +name = "zc.buildout" +setup( + name = name, + version = "1.0.0b2", + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "System for managing development buildouts", + long_description=open('README.txt').read(), + license = "ZPL 2.1", + keywords = "development build", + url='http://svn.zope.org/zc.buildout', + + data_files = [('.', ['README.txt'])], + packages = ['zc', 'zc.buildout'], + package_dir = {'': 'src'}, + namespace_packages = ['zc'], + install_requires = 'setuptools', + include_package_data = True, + tests_require = ['zope.testing'], + test_suite = name+'.tests.test_suite', + entry_points = {'console_scripts': + ['buildout = %s.buildout:main' % name]}, + dependency_links = ['http://download.zope.org/distribution/'], + ) diff --git a/zc.setuptools/specifications/README.txt b/zc.setuptools/specifications/README.txt new file mode 100644 index 00000000..ece00be7 --- /dev/null +++ b/zc.setuptools/specifications/README.txt @@ -0,0 +1,4 @@ +This directory is (an experimental) place to manage Launchpad +specification bodies. Files here are referenced (via svn.zope.org +URLs) from the buildout project specifications in Launchpad, +https://features.launchpad.net/products/zc.buildout. diff --git a/zc.setuptools/specifications/repeatable.txt b/zc.setuptools/specifications/repeatable.txt new file mode 100644 index 00000000..9a3d0364 --- /dev/null +++ b/zc.setuptools/specifications/repeatable.txt @@ -0,0 +1,100 @@ +Repeatable (taggable) buildouts +=============================== + +It's important to be able to tag a buildout in a software repository +in such a way that, months, or even years later, the buildout tag can +be checked out and used to construct the same collection of parts, +with the same versions. (Note that parts could still behave +differently due to changes in parts of the environment, such as system +libraries, not controlled by the buildout.) + +A feature of the buildout is it's use of eggs and the automatic +resolution of dependencies. The latest versions of dependencies are +automatically downloaded and installed. This is great during +development or when using the buildout for casual software +development, but it doesn't work very well for reproducing an old +buildout. + +What's needed is some way to, when needed, record information about +the versions of eggs (and any other bits) who's versions are +determined dynamically. + +Proposal +-------- + +We'll add a buildout option, create-repeatable. The option will +specify a file into which option information should be saved to create +a repeatable buildout. The data will be saved in a form that can be +used by the buildout or recipes in a later run. To make a tagged +buildout, a user would run the buildout with the create-repeatable +option set to a file name and then modify the buildout to be +extended-by this file. + +Consider the following example buildout.cfg:: + + [buildout] + parts = foo + + [foo] + recipe = zc.recipe.eggs + eggs = foo + eek + +Now assume that: + +- The current version of foo is 1.1 + +- Foo depends on bar =, which depends on baz. The current versions of + bar and bas are 1.1 and 2.1. + +- The current version of eek is 1.5 + +- eek depends on ook, which is as version 1.3. + +- zc.recipe.egg is at version 1.0b5 + +If we run the buildout with the command-line option:: + + buildout:create-repeatable=reapeatable.cfg + +we'll get a repeatable.cfg file that looks something like:: + + [foo] + recipe = zc.recipe.eggs ==1.0b5 + static = true + eggs = foo ==1.1 + bar ==1.1 + baz ==2.1 + eek ==1.5 + ook ==1.3 + +The file contains options for the foo part. The buildout software +itself added an entry for the recipe that fixes the recipe version +at the version used by the buildout. + +The zc.recipe.eggs recipe added the eggs option that lists the +specifoc releases that were assembled. + +Finally the buildout.cfg file can be modified to use the +repeatable.cfg file:: + + [buildout] + parts = foo + extended-by: repeatable.cfg + + [foo] + recipe = zc.recipe.eggs + eggs = foo + eek + +When the buildout is run, the options in repeatable.cfg will override +the onces in buildout.cfg, providing a repeatable buildout + +Python API +---------- + +The recipe API will grow a repeatable method that is called after the +install method and is passed a dictionary that a recipe can store +option data in. A recipe instance will only be able to provide repeatable data +for it's part. + diff --git a/zc.setuptools/src/zc/__init__.py b/zc.setuptools/src/zc/__init__.py new file mode 100644 index 00000000..35cf25b7 --- /dev/null +++ b/zc.setuptools/src/zc/__init__.py @@ -0,0 +1,5 @@ +try: + __import__('pkg_resources').declare_namespace(__name__) +except: + # bootstrapping + pass diff --git a/zc.setuptools/src/zc/buildout/__init__.py b/zc.setuptools/src/zc/buildout/__init__.py new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/__init__.py @@ -0,0 +1 @@ +# diff --git a/zc.setuptools/src/zc/buildout/buildout.py b/zc.setuptools/src/zc/buildout/buildout.py new file mode 100644 index 00000000..1fcee087 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/buildout.py @@ -0,0 +1,711 @@ +############################################################################# +# +# 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 logging +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 + +class UserError(Exception): + """Errors made by a user + """ + + def __str__(self): + return " ".join(map(str, self)) + +class MissingOption(UserError, KeyError): + """A required option was missing + """ + +class MissingSection(UserError, KeyError): + """A required section is missinh + """ + +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: + raise MissingOption("Missing option: %s:%s" + % (self.section, option)) + + # XXX need test + def __setitem__(self, option, value): + if not isinstance(value, str): + raise TypeError('Option values must be strings', value) + super(Options, self).__setitem__(option, value) + + def copy(self): + return Options(self.buildout, self.section, self) + +class Buildout(dict): + + def __init__(self, config_file, cloptions): + config_file = os.path.abspath(config_file) + self._config_file = config_file + if not os.path.exists(config_file): + print 'Warning: creating', config_file + open(config_file, 'w').write('[buildout]\nparts = \n') + + super(Buildout, self).__init__() + + # default options + data = dict(buildout={ + 'directory': os.path.dirname(config_file), + 'eggs-directory': 'eggs', + 'develop-eggs-directory': 'develop-eggs', + 'bin-directory': 'bin', + 'parts-directory': 'parts', + 'installed': '.installed.cfg', + 'python': 'buildout', + 'executable': sys.executable, + 'log-level': 'WARNING', + 'log-format': '%(name)s: %(message)s', + }) + + # load user defaults, which override defaults + if 'HOME' in os.environ: + user_config = os.path.join(os.environ['HOME'], + '.buildout', 'default.cfg') + if os.path.exists(user_config): + _update(data, _open(os.path.dirname(user_config), user_config, + [])) + + # load configuration files + _update(data, _open(os.path.dirname(config_file), config_file, [])) + + # apply command-line options + for (section, option, value) in cloptions: + options = data.get(section) + if options is None: + options = self[section] = {} + options[option] = value + # The egg dire + + # do substitutions + 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 + + # copy data into self: + for section, options in data.iteritems(): + self[section] = Options(self, section, options) + + # initialize some attrs and buildout directories. + options = self['buildout'] + + links = options.get('find-links', '') + self._links = links and links.split() or () + + self._buildout_dir = options['directory'] + for name in ('bin', 'parts', 'eggs', 'develop-eggs'): + d = self._buildout_path(options[name+'-directory']) + options[name+'-directory'] = d + + options['installed'] = os.path.join(options['directory'], + options['installed']) + + self._setup_logging() + + 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 UserError("Circular reference in substitutions.\n" + "We're evaluating %s\nand are referencing: %s.\n" + % (", ".join([":".join(k) for k in seen]), + ":".join(key) + ) + ) + seen.append(key) + value = '$$'.join([self._dosubs_esc(s, data, converted, seen) + for s in value.split('$$') + ]) + seen.pop() + return value + + _template_split = re.compile('([$]{[^}]*})').split + _simple = re.compile('[-a-zA-Z0-9 ._]+$').match + _valid = re.compile('[-a-zA-Z0-9 ._]+:[-a-zA-Z0-9 ._]+$').match + def _dosubs_esc(self, value, data, converted, seen): + value = self._template_split(value) + subs = [] + for ref in value[1::2]: + s = tuple(ref[2:-1].split(':')) + if not self._valid(ref): + if len(s) < 2: + raise UserError("The substitution, %s,\n" + "doesn't contain a colon." + % ref) + if len(s) > 2: + raise UserError("The substitution, %s,\n" + "has too many colons." + % ref) + if not self._simple(s[0]): + raise UserError("The section name in substitution, %s,\n" + "has invalid characters." + % ref) + if not self._simple(s[1]): + raise UserError("The option name in substitution, %s,\n" + "has invalid characters." + % ref) + + v = converted.get(s) + if v is None: + options = data.get(s[0]) + if options is None: + raise MissingSection( + "Referenced section does not exist", s[0]) + v = options.get(s[1]) + if v is None: + raise MissingOption("Referenced option does not exist:", + *s) + if '$' in v: + v = self._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)]) + + def _buildout_path(self, *names): + return os.path.join(self._buildout_dir, *names) + + def bootstrap(self, args): + self._setup_directories() + + # Now copy buildout and setuptools eggs, amd record destination eggs: + entries = [] + for name in 'setuptools', 'zc.buildout': + r = pkg_resources.Requirement.parse(name) + dist = pkg_resources.working_set.find(r) + if dist.precedence == pkg_resources.DEVELOP_DIST: + dest = os.path.join(self['buildout']['eggs-directory'], + name+'.egg-link') + open(dest, 'w').write(dist.location) + entries.append(dist.location) + else: + dest = os.path.join(self['buildout']['eggs-directory'], + os.path.basename(dist.location)) + entries.append(dest) + if not os.path.exists(dest): + if os.path.isdir(dist.location): + shutil.copytree(dist.location, dest) + else: + shutil.copy2(dist.location, dest) + + # Create buildout script + ws = pkg_resources.WorkingSet(entries) + ws.require('zc.buildout') + zc.buildout.easy_install.scripts( + ['zc.buildout'], ws, sys.executable, + self['buildout']['bin-directory']) + + def install(self, install_parts): + self._setup_directories() + + # Add develop-eggs directory to path so that it gets searched + # for eggs: + sys.path.insert(0, self['buildout']['develop-eggs-directory']) + + # Build develop eggs + self._develop() + + # load installed data + installed_part_options = self._read_installed_part_options() + + # get configured and installed part lists + conf_parts = self['buildout']['parts'] + conf_parts = conf_parts and conf_parts.split() or [] + installed_parts = installed_part_options['buildout']['parts'] + installed_parts = installed_parts and installed_parts.split() or [] + + + # If install_parts is given, then they must be listed in parts + # and we don't uninstall anything. Otherwise, we install + # the configured parts and uninstall anything else. + if install_parts: + extra = [p for p in install_parts if p not in conf_parts] + if extra: + self._error('Invalid install parts:', *extra) + uninstall_missing = False + else: + install_parts = conf_parts + uninstall_missing = True + + # load recipes + recipes = self._load_recipes(install_parts) + + # compute new part recipe signatures + self._compute_part_signatures(install_parts) + + try: + # uninstall parts that are no-longer used or who's configs + # have changed + for part in reversed(installed_parts): + if part in install_parts: + old_options = installed_part_options[part].copy() + old_options.pop('__buildout_installed__') + new_options = self.get(part) + if old_options == new_options: + continue + for k in old_options: + if k not in new_options: + self._logger.debug("Part: %s, dropped option %s", + part, k) + elif old_options[k] != new_options[k]: + self._logger.debug( + "Part: %s, option %s, %r != %r", + part, k, new_options[k], old_options[k], + ) + for k in new_options: + if k not in old_options: + self._logger.debug("Part: %s, new option %s", + part, k) + elif not uninstall_missing: + continue + + # ununstall part + self._logger.info('Uninstalling %s', part) + self._uninstall( + installed_part_options[part]['__buildout_installed__']) + installed_parts = [p for p in installed_parts if p != part] + + # install new parts + for part in install_parts: + self._logger.info('Installing %s', part) + installed_part_options[part] = self[part].copy() + del self[part]['__buildout_signature__'] + installed_files = recipes[part].install() or () + if isinstance(installed_files, str): + installed_files = [installed_files] + installed_part_options[part]['__buildout_installed__'] = ( + '\n'.join(installed_files) + ) + if part not in installed_parts: + installed_parts.append(part) + finally: + installed_part_options['buildout']['parts'] = ' '.join( + [p for p in conf_parts if p in installed_parts] + + + [p for p in installed_parts if p not in conf_parts] + ) + self._save_installed_options(installed_part_options) + + def _setup_directories(self): + + # Create buildout directories + for name in ('bin', 'parts', 'eggs', 'develop-eggs'): + d = self['buildout'][name+'-directory'] + if not os.path.exists(d): + self._logger.info('Creating directory %s', d) + os.mkdir(d) + + 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') + self._logger.info("Running %s -q develop ...", setup) + os.chdir(os.path.dirname(setup)) + os.spawnle( + os.P_WAIT, sys.executable, sys.executable, + zc.buildout.easy_install._safe_arg(setup), + '-q', 'develop', '-m', '-x', '-N', + '-f', zc.buildout.easy_install._safe_arg( + ' '.join(self._links) + ), + '-d', zc.buildout.easy_install._safe_arg( + self['buildout']['develop-eggs-directory'] + ), + {'PYTHONPATH': + os.path.dirname(pkg_resources.__file__)}, + ) + finally: + os.chdir(here) + + def _load_recipes(self, parts): + recipes = {} + if not parts: + return recipes + + recipes_requirements = [] + pkg_resources.working_set.add_entry( + self['buildout']['develop-eggs-directory']) + pkg_resources.working_set.add_entry(self['buildout']['eggs-directory']) + + # Gather requirements + for part in parts: + options = self.get(part) + if options is None: + raise MissingSection("No section was specified for part", part) + + recipe, entry = self._recipe(part, options) + recipes_requirements.append(recipe) + + # Install the recipe distros + offline = self['buildout'].get('offline', 'false') + if offline not in ('true', 'false'): + self._error('Invalif value for offline option: %s', offline) + if offline == 'true': + ws = zc.buildout.easy_install.working_set( + recipes_requirements, sys.executable, + [self['buildout']['eggs-directory'], + self['buildout']['develop-eggs-directory'], + ], + ) + else: + ws = zc.buildout.easy_install.install( + recipes_requirements, self['buildout']['eggs-directory'], + links=self._links, index=self['buildout'].get('index'), + path=[self['buildout']['develop-eggs-directory']]) + + # Add the distros to the working set + pkg_resources.require(recipes_requirements) + + # instantiate the recipes + for part in parts: + options = self[part] + recipe, entry = self._recipe(part, options) + recipe_class = pkg_resources.load_entry_point( + recipe, 'zc.buildout', entry) + recipes[part] = recipe_class(self, part, options) + + return recipes + + def _compute_part_signatures(self, parts): + # Compute recipe signature and add to options + base = self['buildout']['eggs-directory'] + os.path.sep + for part in parts: + options = self.get(part) + if options is None: + options = self[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) + + def _recipe(self, part, options): + recipe = options['recipe'] + 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(_spacey_defaults) + parser.optionxform = lambda s: s + parser.read(old) + return dict([ + (section, + Options(self, section, + [item for item in parser.items(section) + if item[0] not in _spacey_defaults] + ) + ) + for section in parser.sections()]) + else: + return {'buildout': Options(self, '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): + f = open(self._installed_path(), 'w') + _save_options('buildout', installed_options['buildout'], f) + for part in installed_options['buildout']['parts'].split(): + print >>f + _save_options(part, installed_options[part], f) + f.close() + + def _error(self, message, *args, **kw): + self._logger.error(message, *args, **kw) + sys.exit(1) + + def _setup_logging(self): + root_logger = logging.getLogger() + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter(self['buildout']['log-format'])) + root_logger.addHandler(handler) + self._logger = logging.getLogger('buildout') + level = self['buildout']['log-level'] + if level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'): + level = getattr(logging, level) + else: + try: + level = int(level) + except ValueError: + self._error("Invalid logging level %s", level) + verbosity = self['buildout'].get('verbosity', 0) + try: + verbosity = int(verbosity) + except ValueError: + self._error("Invalid verbosity %s", verbosity) + + level -= verbosity + root_logger.setLevel(level) + + if level <= logging.DEBUG: + sections = list(self) + sections.sort() + print 'Configuration data:' + for section in sections: + _save_options(section, self[section], sys.stdout) + print + +_spacey_nl = re.compile('[ \t\r\f\v]*\n[ \t\r\f\v\n]*' + '|' + '^[ \t\r\f\v]+' + '|' + '[ \t\r\f\v]+$' + ) + +def _quote_spacey_nl(match): + match = match.group(0).split('\n', 1) + result = '\n\t'.join( + [(s + .replace(' ', '%(__buildout_space__)s') + .replace('\r', '%(__buildout_space_r__)s') + .replace('\f', '%(__buildout_space_f__)s') + .replace('\v', '%(__buildout_space_v__)s') + .replace('\n', '%(__buildout_space_n__)s') + ) + for s in match] + ) + return result + +_spacey_defaults = dict( + __buildout_space__ = ' ', + __buildout_space_r__ = '\r', + __buildout_space_f__ = '\f', + __buildout_space_v__ = '\v', + __buildout_space_n__ = '\n', + ) + +def _save_options(section, options, f): + print >>f, '[%s]' % section + items = options.items() + items.sort() + for option, value in items: + value = value.replace('%', '%%') + value = _spacey_nl.sub(_quote_spacey_nl, value) + if value.startswith('\n\t'): + value = '%(__buildout_space_n__)s' + value[2:] + if value.endswith('\n\t'): + value = value[:-2] + '%(__buildout_space_n__)s' + print >>f, option, '=', value + + + +def _open(base, filename, seen): + """Open a configuration file and return the result as a dictionary, + + Recursively open other files based on buildout options found. + """ + + filename = os.path.join(base, filename) + if filename in seen: + raise UserError("Recursive file include", seen, filename) + + base = os.path.dirname(filename) + seen.append(filename) + + result = {} + + parser = ConfigParser.SafeConfigParser() + parser.optionxform = lambda s: s + parser.readfp(open(filename)) + extends = extended_by = None + for section in parser.sections(): + options = dict(parser.items(section)) + if section == 'buildout': + extends = options.pop('extends', extends) + extended_by = options.pop('extended-by', extended_by) + result[section] = options + + if extends: + extends = extends.split() + extends.reverse() + for fname in extends: + result = _update(_open(base, fname, seen), result) + + if extended_by: + for fname in extended_by.split(): + result = _update(result, _open(base, fname, seen)) + + seen.pop() + return result + + +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 _update(d1, d2): + for section in d2: + if section in d1: + d1[section].update(d2[section]) + else: + d1[section] = d2[section] + return d1 + +def _error(*message): + sys.stderr.write('Error: ' + ' '.join(message) +'\n') + sys.exit(1) + +def main(args=None): + if args is None: + args = sys.argv[1:] + + config_file = 'buildout.cfg' + verbosity = 0 + options = [] + while args: + if args[0][0] == '-': + op = orig_op = args.pop(0) + op = op[1:] + while op and op[0] in 'vq': + if op[0] == 'v': + verbosity += 10 + else: + verbosity -= 10 + op = op[1:] + + if op[:1] == 'c': + op = op[1:] + if op: + config_file = op + else: + if args: + config_file = args.pop(0) + else: + _error("No file name specified for option", orig_op) + elif op: + _error("Invalid option", '-'+op[0]) + elif '=' in args[0]: + option, value = args.pop(0).split('=', 1) + if len(option.split(':')) != 2: + _error('Invalid option:', option) + section, option = option.split(':') + options.append((section.strip(), option.strip(), value.strip())) + else: + # We've run out of command-line options and option assignnemnts + # The rest should be commands, so we'll stop here + break + + if verbosity: + options.append(('buildout', 'verbosity', str(verbosity))) + + if args: + command = args.pop(0) + if command not in ('install', 'bootstrap'): + _error('invalid command:', command) + else: + command = 'install' + + try: + try: + buildout = Buildout(config_file, options) + getattr(buildout, command)(args) + except UserError, v: + _error(str(v)) + + finally: + logging.shutdown() + +if sys.version_info[:2] < (2, 4): + def reversed(iterable): + result = list(iterable); + result.reverse() + return result diff --git a/zc.setuptools/src/zc/buildout/buildout.txt b/zc.setuptools/src/zc/buildout/buildout.txt new file mode 100644 index 00000000..898b85c1 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/buildout.txt @@ -0,0 +1,1201 @@ +Buildouts +========= + +The word "buildout" refers to a description of a set of parts and the +software to create and 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". + +This document describes how to define buildouts using buildout +configuration files and recipes. There are three ways to set up the +buildout software and create a buildout instance: + +1. Install the zc.buildout egg with easy_install and use the buildout + script installed in a Python scripts area. + +2. Use the buildout bootstrap script to create a buildout that + includes both the setuptools and zc.buildout eggs. This allows you + to use the buildout software without modifying a Python install. + The buildout script is installed into your buildout local scripts + area. + +3. Use a buildoput command from an already installed buildout to + bootstrap a new buildout. (See the section on bootstraping later + in this document.) + +Often, a software project will be managed in a software repository, +such as a subversion repository, that includes some software source +directories, buildout configuration files, and a copy of the buildout +bootstrap script, To work on the project, one would check out the +project from the repository and run the bootstrap script which +installs setuptools and zc.buildout into the checkout as well as any +parts defined. + +We have a sample buildout that we created using the bootstrap command +of an existing buildout (method 3 above). It has the absolute minimum +information. We have bin, develop-eggs, eggs and parts directories, +and a configuration file: + + >>> ls(sample_buildout) + d bin + - buildout.cfg + d develop-eggs + d eggs + d parts + +The bin directory contains scripts. + + >>> ls(sample_buildout, 'bin') + - buildout + - py-zc.buildout + + >>> ls(sample_buildout, 'eggs') + - setuptools-0.6-py2.4.egg + - zc.buildout-1.0-py2.4.egg + +The develop-eggs and parts directories are initially empty: + + >>> ls(sample_buildout, 'develop-eggs') + >>> ls(sample_buildout, 'parts') + +The develop-eggs directory holds egg links for software being +developed in the buildout. We separate develop-eggs and other eggs to +allow eggs directories to be shared across multiple buildouts. For +example, a common developer technique is to define a common eggs +directory in their home that all non-develop eggs are stored in. This +allows larger buildouts to be set up much more quickly and saves disk +space. + +The parts directory provides an area where recipes 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 +sub-directory of the parts directory with the same name as the part. + +Buildouts are defined using configuration files. These are in the +format defined by the Python ConfigParser module, with extensions +that we'll describe later. By default, when a buildout is run, it +looks for the file buildout.cfg in the directory where the buildout is +run. + +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 +even a configuration file. + +A part is created by a recipe. Recipes are always installed as Python +eggs. They can be downloaded from a 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 logging, os + ... + ... class Mkdir: + ... + ... def __init__(self, buildout, name, options): + ... self.buildout = buildout + ... self.name = name + ... self.options = options + ... options['path'] = os.path.join( + ... buildout['buildout']['directory'], + ... options['path'], + ... ) + ... + ... def install(self): + ... path = self.options['path'] + ... if not os.path.isdir(path): + ... logging.getLogger(self.name).info( + ... 'Creating directory %s', 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. + +If the path is relative, we'll interpret it as relative to the +buildout directory. The buildout object passed in is a mapping from +section name to a mapping of options for that section. The buildout +directory is available as the directory option of the buildout +section. We normalize the path and save it back into the options +directory. + +Any time we use data from another section, it is important to reflect +that data in the recipe's options when the recipe is constructed. + +When buildout is run, it saves configuration data for installed parts +in a file named installed.cfg. In subsequent runs, it compares +part-configuration data stored in the installed.cfg file and the +part-configuration data loaded from the configuration files as +modified by recipe constructors to decide if the configuration of a +part has changed. If the configuration has changed, or if the recipe +has changed, then the part is uninstalled before reinstalling it. The +buildout only looks at the part's options, so any data used to +configure the part needs to be reflected in the part's options. It is +the job of a recipe constructor to make sure that the options include +all rel event data. + +Of course, parts are also uninstalled if they are no-longer used. + +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 +path option from our options dictionary. + +The install method logs what it's doing using the Python logging call. + +We return the path that we installed. If the part is uninstalled 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. For most recipes, this is all of the uninstall +support needed. A recipe can provide custom uninstall support as will +be described later. + +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']}, + ... ) + ... """) + +This setup script is incomplete. It doesn't describe what is to be +included in a distribution. This is fine if we never actually create +a distribution. If recipes are going to be used only internally in a +buildout, then we needn't include distribution information. If we +wanted to use the same recipes in multiple buildouts, then we'd need +to include proper distribution data. To find out more about creating +distributions, see the setuptools documentation. + +Our setup script defines 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 an annoying warning +from distutils, on which setuptools and zc.buildout are based: + + >>> write(sample_buildout, 'recipes', 'README.txt', " ") + +Now let's update our buildout.cfg: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = data-dir + ... log-level = INFO + ... + ... [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 buildout +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 file. +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. + +:: + + log-level = INFO + +The default level is WARNING, which is fairly quite. In this example, +we set the level to INFO so we can see more details about what the +buildout and recipes are doing. + +:: + + [data-dir] + recipe = recipes:mkdir + path = mystuff + + +When we name a part, we also create a section of the same +name that contains part data. In this section, we'll 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) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(buildout), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Installing data-dir + data-dir: Creating directory mystuff + +We see that the recipe created the directory, as expected: + + >>> ls(sample_buildout) + - .installed.cfg + d bin + - buildout.cfg + d develop-eggs + d eggs + d mystuff + d parts + d recipes + +In addition, .installed.cfg has been created containing information +about the part we installed: + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = data-dir + <BLANKLINE> + [data-dir] + __buildout_installed__ = /tmp/sample-buildout/mystuff + __buildout_signature__ = recipes-c7vHV6ekIDUPy/7fjAaYjg== + path = /tmp/sample-buildout/mystuff + recipe = recipes:mkdir + +Note that the directory we installed is included in .installed.cfg. +In addition, the path option includes the actual destination +directory. + +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 + ... log-level = INFO + ... + ... [data-dir] + ... recipe = recipes:mkdir + ... path = mydata + ... """) + + >>> print system(buildout), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling data-dir + buildout: Installing data-dir + data-dir: Creating directory mydata + + >>> ls(sample_buildout) + - .installed.cfg + d bin + - buildout.cfg + d develop-eggs + d eggs + d mydata + d parts + d recipes + +Configuration file syntax +------------------------- + +As mentioned earlier, buildout configuration files use the format +defined by the Python ConfigParser module with extensions. The +extensions are: + +- option names are case sensitive + +- option values can ue a substitution syntax, described below, to + refer to option values in specific sections. + +The ConfigParser syntax is very flexible. Section names can contain +any characters other than newlines and right square braces ("]"). +Option names can contain any characters other than newlines, colons, +and equal signs, can not start with a space, and don't include +trailing spaces. + +It is likely that, in the future, some characters will be given +special buildout-defined meanings. This is already true of the +characters ":", "$", "%", "(", and ")". For now, it is a good idea to +keep section and option names simple, sticking to alphanumeric +characters, hyphens, and periods. + +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. In particular, entry points are now defined as a configuration +string, rather than a dictionary. + +Let's update our configuration to provide variable substitution +examples: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = data-dir debug + ... log-level = INFO + ... + ... [debug] + ... recipe = recipes:debug + ... File 1 = ${data-dir:path}/file + ... File 2 = %(File 1)s.out + ... File 3 = %(base)s/file3 + ... File 4 = ${debug:File 3}/log + ... + ... [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 options in the same section and options defined in the +DEFAULT section. + +We used a string-template substitution for file1. This type of +substitution uses the string.Template syntax. Names substituted 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(buildout), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling data-dir + buildout: Installing data-dir + data-dir: Creating directory mydata + buildout: Installing debug + File 1 mydata/file + File 2 mydata/file.out + File 3 var/file3 + File 4 var/file3/log + base var + 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(buildout), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Installing data-dir + buildout: Installing debug + File 1 mydata/file + File 2 mydata/file.out + File 3 var/file3 + File 4 var/file3/log + base var + recipe recipes:debug + +We can see that mydata was not recreated. + +Note that, in this case, we didn't specify a log level, so +we didn't get output about what the buildout was doing. + +Section and option names in variable substitutions are only allowed to +contain alphanumeric characters, hyphens, periods and spaces. This +restriction might be relaxed in future releases. + +Multiple configuration files +---------------------------- + +You can use multiple configuration files. From your main +configuration file, you can include other configuration files in 2 +ways: + +- Your configuration file can "extend" another configuration file. + Option are read from the other configuration file if they aren't + already defined by your configuration file. + +- Your configuration file can be "extended-by" another configuration + file, In this case, the options in the other configuration file + override options in your configuration file. + +The configuration files your file extends or is extended by can extend +or be extended by other configuration files. The same file may be +used more than once although, of course, cycles aren't allowed. + +To see how this works, we use an example: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... extends = base.cfg + ... + ... [debug] + ... op = buildout + ... """) + + >>> write(sample_buildout, 'base.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = debug + ... + ... [debug] + ... recipe = recipes:debug + ... op = base + ... """) + + >>> print system(buildout), + op buildout + recipe recipes:debug + +The example is pretty trivial, but the pattern it illustrates is +pretty common. In a more practical example, the base buildout might +represent a product and the extending buildout might be a +customization. + +Here is a more elaborate example. + + >>> extensions = mkdtemp() + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... extends = b1.cfg b2.cfg + ... extended-by = e1.cfg %(e2)s + ... + ... [debug] + ... op = %%(name)s + ... + ... [DEFAULT] + ... name = buildout + ... """ % dict(e2=os.path.join(extensions, 'e2.cfg'))) + + >>> write(sample_buildout, 'b1.cfg', + ... """ + ... [buildout] + ... extends = base.cfg + ... + ... [debug] + ... op1 = %(name)s 1 + ... op2 = %(name)s 2 + ... op3 = %(name)s 3 + ... + ... [DEFAULT] + ... name = b1 + ... """) + + >>> write(sample_buildout, 'b2.cfg', + ... """ + ... [buildout] + ... extends = base.cfg + ... + ... [debug] + ... op3 = %(name)s 3 + ... op4 = %(name)s 4 + ... op5 = %(name)s 5 + ... + ... [DEFAULT] + ... name = b2 + ... """) + + >>> write(sample_buildout, 'base.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = debug + ... + ... [debug] + ... recipe = recipes:debug + ... name = base + ... """) + + >>> write(sample_buildout, 'e1.cfg', + ... """ + ... [debug] + ... op1 = %(name)s 1 + ... + ... [DEFAULT] + ... name = e1 + ... """) + + >>> write(extensions, 'e2.cfg', + ... """ + ... [buildout] + ... extends = eb.cfg + ... extended-by = ee.cfg + ... """) + + >>> write(extensions, 'eb.cfg', + ... """ + ... [debug] + ... op5 = %(name)s 5 + ... + ... [DEFAULT] + ... name = eb + ... """) + + >>> write(extensions, 'ee.cfg', + ... """ + ... [debug] + ... op6 = %(name)s 6 + ... + ... [DEFAULT] + ... name = ee + ... """) + + >>> print system(buildout), + name ee + op buildout + op1 e1 1 + op2 b1 2 + op3 b2 3 + op4 b2 4 + op5 eb 5 + op6 ee 6 + recipe recipes:debug + +There are several things to note about this example: + +- We can name multiple files in an extends or extended-by option. + +- We can reference files recursively. + +- DEFAULT sections only directly affect the configuration file they're + used in, but they can have secondary effects. For example, the name + option showed up in the debug section because it was defined in the + debug sections in several of the input files by virtue of being in + their DEFAULT sections. + +- Relative file names in extended and extended-by options are + interpreted relative to the directory containing the referencing + configuration file. The files eb.cfg and ee.cfg were found in the + extensions directory because they were referenced from a file in + that directory. + +User defaults +------------- + +If the file $HOME/.buildout/defaults.cfg, exists, it is read before +reading the configuration file. ($HOME is the value of the HOME +environment variable. The '/' is replaced by the operating system file +delimiter.) + + >>> home = mkdtemp() + >>> mkdir(home, '.buildout') + >>> write(home, '.buildout', 'default.cfg', + ... """ + ... [debug] + ... op1 = 1 + ... op7 = 7 + ... """) + + >>> os.environ['HOME'] = home + >>> print system(buildout), + name ee + op buildout + op1 e1 1 + op2 b1 2 + op3 b2 3 + op4 b2 4 + op5 eb 5 + op6 ee 6 + op7 7 + recipe recipes:debug + + >>> del os.environ['HOME'] + +Command-line usage +------------------ + +A number of arguments can be given on the buildout command line. The +command usage is:: + + buildout [-c file] [-q] [-v] [assignments] [command [command arguments]] + +The -c option can be used to specify a configuration file, rather than +buildout.cfg in the current directory. + +The -q and -v decrement and increment the verbosity by 10. The +verbosity is used to adjust the logging level. The verbosity is +subtracted from the numeric value of the log-level option specified in +the configuration file. + +Assignments are of the form:: + + section_name:option_name=value + +Options and assignments can be given in any order. + +Here's an example: + + >>> write(sample_buildout, 'other.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = debug + ... installed = .other.cfg + ... + ... [debug] + ... name = other + ... recipe = recipes:debug + ... """) + +Note that we used the installed buildout option to specify an +alternate file to store information about installed parts. + + >>> print system(buildout+' -c other.cfg debug:op1=foo -v'), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Installing debug + name other + op1 foo + recipe recipes:debug + +Here we used the -c option to specify an alternate configuration file, +and the -v option to increase the level of logging from the default, +WARNING. + +Options can also be combined in the usual Unix way, as in: + + >>> print system(buildout+' -vcother.cfg debug:op1=foo'), + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Installing debug + name other + op1 foo + recipe recipes:debug + +Here we combined the -v and -c options with the configuration file +name. Note that the -c option has to be last, because it takes an +argument. + + >>> os.remove(os.path.join(sample_buildout, 'other.cfg')) + >>> os.remove(os.path.join(sample_buildout, '.other.cfg')) + +The most commonly used command is 'install' and it takes a +list of parts to install. if any parts are specified, then they must +be listed in the buildout parts option and only those parts are +installed. To illustrate this, we'll update our configuration and run +the buildout in the usual way: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = debug d1 d2 d3 + ... + ... [d1] + ... recipe = recipes:mkdir + ... path = d1 + ... + ... [d2] + ... recipe = recipes:mkdir + ... path = d2 + ... + ... [d3] + ... recipe = recipes:mkdir + ... path = d3 + ... + ... [debug] + ... recipe = recipes:debug + ... """) + + >>> print system(buildout+' -v'), + buildout: Running /sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling debug + buildout: Installing debug + recipe recipes:debug + buildout: Installing d1 + d1: Creating directory d1 + buildout: Installing d2 + d2: Creating directory d2 + buildout: Installing d3 + d3: Creating directory d3 + + >>> ls(sample_buildout) + - .installed.cfg + - b1.cfg + - b2.cfg + - base.cfg + d bin + - buildout.cfg + d d1 + d d2 + d d3 + d develop-eggs + - e1.cfg + d eggs + d parts + d recipes + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = debug d1 d2 d3 + <BLANKLINE> + [debug] + __buildout_installed__ = + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + recipe = recipes:debug + <BLANKLINE> + [d1] + __buildout_installed__ = /tmp/sample-buildout/d1 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/d1 + recipe = recipes:mkdir + <BLANKLINE> + [d2] + __buildout_installed__ = /tmp/sample-buildout/d2 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/d2 + recipe = recipes:mkdir + <BLANKLINE> + [d3] + __buildout_installed__ = /tmp/sample-buildout/d3 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/d3 + recipe = recipes:mkdir + +Now we'll update our configuration file: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = debug d2 d3 d4 + ... + ... [d2] + ... recipe = recipes:mkdir + ... path = data2 + ... + ... [d3] + ... recipe = recipes:mkdir + ... path = data3 + ... + ... [d4] + ... recipe = recipes:mkdir + ... path = data4 + ... + ... [debug] + ... recipe = recipes:debug + ... x = 1 + ... """) + +and run the buildout specifying just d3 and d4: + + >>> print system(buildout+' -v install d3 d4'), + buildout: Running /sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling d3 + buildout: Installing d3 + d3: Creating directory data3 + buildout: Installing d4 + d4: Creating directory data4 + + >>> ls(sample_buildout) + - .installed.cfg + - b1.cfg + - b2.cfg + - base.cfg + d bin + - buildout.cfg + d d1 + d d2 + d data3 + d data4 + d develop-eggs + - e1.cfg + d eggs + d parts + d recipes + +Only the d3 and d4 recipes ran. d3 was removed and data3 and data4 +were created. + +The .installed.cfg is only updated for the recipes that ran: + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = debug d2 d3 d4 d1 + <BLANKLINE> + [debug] + __buildout_installed__ = + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + recipe = recipes:debug + <BLANKLINE> + [d2] + __buildout_installed__ = /tmp/sample-buildout/d2 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/d2 + recipe = recipes:mkdir + <BLANKLINE> + [d3] + __buildout_installed__ = /tmp/sample-buildout/data3 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/data3 + recipe = recipes:mkdir + <BLANKLINE> + [d4] + __buildout_installed__ = /tmp/sample-buildout/data4 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/data4 + recipe = recipes:mkdir + <BLANKLINE> + [d1] + __buildout_installed__ = /tmp/sample-buildout/d1 + __buildout_signature__ = recipes-PiIFiO8ny5yNZ1S3JfT0xg== + path = /tmp/sample-buildout/d1 + recipe = recipes:mkdir + +Note that the installed data for debug, d1, and d2 haven't changed, +because we didn't install those parts and that the d1 and d2 +directories are still there. + +Now, if we run the buildout without the install command: + + >>> print system(buildout+' -v'), + buildout: Running /sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling d1 + buildout: Uninstalling d2 + buildout: Uninstalling debug + buildout: Installing debug + recipe recipes:debug + x 1 + buildout: Installing d2 + d2: Creating directory data2 + buildout: Installing d3 + buildout: Installing d4 + +We see the output of the debug recipe and that data2 was created. We +also see that d1 and d2 have gone away: + + >>> ls(sample_buildout) + - .installed.cfg + - b1.cfg + - b2.cfg + - base.cfg + d bin + - buildout.cfg + d data2 + d data3 + d data4 + d develop-eggs + - e1.cfg + d eggs + d parts + d recipes + +Alternate directory and file locations +-------------------------------------- + +The buildout normally puts the bin, eggs, and parts directories in the +directory in the directory containing the configuration file. You can +provide alternate locations, and even names for these directories. + + >>> alt = mkdtemp('sample-alt') + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = + ... develop-eggs-directory = %(developbasket)s + ... eggs-directory = %(basket)s + ... bin-directory = %(scripts)s + ... parts-directory = %(work)s + ... """ % dict( + ... developbasket = os.path.join(alt, 'developbasket'), + ... basket = os.path.join(alt, 'basket'), + ... scripts = os.path.join(alt, 'scripts'), + ... work = os.path.join(alt, 'work'), + ... )) + + >>> print system(buildout+' -v'), + buildout: Creating directory /tmp/sample-alt/scripts + buildout: Creating directory /tmp/sample-alt/work + buildout: Creating directory /tmp/sample-alt/basket + buildout: Creating directory /sample-alt/developbasket + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + buildout: Uninstalling d4 + buildout: Uninstalling d3 + buildout: Uninstalling d2 + buildout: Uninstalling debug + + >>> ls(alt) + d basket + d developbasket + d scripts + d work + + >>> ls(alt, 'developbasket') + - recipes.egg-link + +You can also specify an alternate buildout directory: + + >>> alt = mkdtemp('sample-alt') + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... directory = %(alt)s + ... develop = %(recipes)s + ... parts = + ... """ % dict( + ... alt=alt, + ... recipes=os.path.join(sample_buildout, 'recipes'), + ... )) + + >>> print system(buildout+' -v'), + buildout: Creating directory /tmp/sample-alt/bin + buildout: Creating directory /tmp/sample-alt/parts + buildout: Creating directory /tmp/sample-alt/eggs + buildout: Creating directory /tmp/sample-alt/develop-eggs + buildout: Running /tmp/sample-buildout/recipes/setup.py -q develop ... + + >>> ls(alt) + - .installed.cfg + d bin + d develop-eggs + d eggs + d parts + + >>> ls(alt, 'develop-eggs') + - recipes.egg-link + +Logging control +--------------- + +Three buildout options are used to control logging: + +log-level + specifies the log level + +verbosity + adjusts the log level + +log-format + allows an alternate logging for mat to be specified + +We've already seen the log level and verbosity. Let's look at an example +of changing the format: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = recipes + ... parts = + ... log-level = 25 + ... verbosity = 5 + ... log-format = %%(levelname)s %%(message)s + ... """) + +Here, we've changed the format to include the log-level name, rather +than the logger name. Note that we had to double percent signs, +because configuration options allow ConfigParser variable substitution. + +We've also illustrated, with a contrived example, that the log level +can be a numeric value and that the verbosity can be specified in the +configuration file. Because the verbosity is subtracted from the log +level, we get a final log level of 20, which is the INFO level. + + >>> print system(buildout), + INFO Running /tmp/sample-buildout/recipes/setup.py -q develop ... + +Predefined buildout options +--------------------------- + +Buildouts have a number of predefined options that recipes can use +and that users can override in their configuration files. To see +these, we'll run a minimal buildout configuration with a debug logging +level. One of the features of debug logging is that the configuration +database is shown. + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = + ... """) + + >>> print system(buildout+' -vv'), + Configuration data: + [buildout] + bin-directory = /tmp/sample-buildout/bin + develop-eggs-directory = /tmp/sample-buildout/develop-eggs + directory = /tmp/sample-buildout + eggs-directory = /tmp/sample-buildout/eggs + executable = /usr/local/bin/python2.3 + installed = /tmp/sample-buildout/.installed.cfg + log-format = %%(name)s: %%(message)s + log-level = WARNING + parts = + parts-directory = /tmp/sample-buildout/parts + python = buildout + verbosity = 20 + <BLANKLINE> + +All of these options can be overridden by configuration files or by +command-line assignments. We've discussed most of these options +already, but let's review them and touch on some we haven't discussed: + +bin-directory + The directory path where scripts are written. This can be a + relative path, which is interpreted relative to the directory + option. + +develop-eggs-directory + The directory path where development egg links are created for software + being created in the local project. This can be a relative path, + which is interpreted relative to the directory option. + +directory + The buildout directory. This is the base for other buildout file + and directory locations, when relative locations are used. + +eggs-directory + The directory path where downloaded eggs are put. It is common to share + this directory across buildouts. Eggs in this directory should + *never* be modified. This can be a relative path, which is + interpreted relative to the directory option. + +executable + The Python executable used to run the buildout. See the python + option below. + +installed + The file path where information about the results of the previous + buildout run is written. This can be a relative path, which is + interpreted relative to the directory option. This file provides + an inventory of installed parts with information needed to decide + which if any parts need to be uninstalled. + +log-format + The format used for logging messages. + +log-level + The log level before verbosity adjustment + +parts + A white space separated list of parts to be installed. + +parts-directory + A working directory that parts can used to store data. + +python + The name of a section containing information about the default + Python interpreter. Recipes that need a installation + typically have options to tell them which Python installation to + use. By convention, if a section-specific option isn't used, the + option is looked for in the buildout section. The option must + point to a section with an executable option giving the path to a + Python executable. By default, the buildout section defines the + default Python as the Python used to run the buildout. + +verbosity + A log-level adjustment. Typically, this is set via the -q and -v + command-line options. + + +Bootstrapping +------------- + +If zc.buildout is installed, you can use it to create a new buildout +with it's own local copies of zc.buildout and setuptools and with +local buildout scripts. + + >>> sample_bootstrapped = mkdtemp('sample-bootstrapped') + + >>> print system(buildout + ... +' -c'+os.path.join(sample_bootstrapped, 'setup.cfg') + ... +' bootstrap'), + Warning: creating /sample-bootstrapped/setup.cfg + +Note that a basic setup.cfg was created for us. + + >>> ls(sample_bootstrapped) + d bin + d develop-eggs + d eggs + d parts + - setup.cfg + + >>> ls(sample_bootstrapped, 'bin') + - buildout + - py-zc.buildout + + >>> ls(sample_bootstrapped, 'eggs') + - setuptools-0.6-py2.3.egg + - zc.buildout-1.0-py2.3.egg + +Note that the buildout script was installed but not run. To run +the buildout, we'd have to run the installed buildout script. diff --git a/zc.setuptools/src/zc/buildout/easy_install.py b/zc.setuptools/src/zc/buildout/easy_install.py new file mode 100644 index 00000000..c7d83bb3 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/easy_install.py @@ -0,0 +1,461 @@ +############################################################################## +# +# 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$ +""" + +import logging, os, re, tempfile, sys +import pkg_resources, setuptools.command.setopt +import zc.buildout + +# XXX we could potentially speed this up quite a bit by keeping our +# own PackageIndex to analyse whether there are newer dists. A hitch +# is that the package index seems to go out of its way to only handle +# one Python version at a time. :( + +logger = logging.getLogger('zc.buildout.easy_install') + +# Include buildout and setuptools eggs in paths +buildout_and_setuptools_path = [ + (('.egg' in m.__file__) + and m.__file__[:m.__file__.rfind('.egg')+4] + or os.path.dirname(m.__file__) + ) + for m in (pkg_resources,) + ] +buildout_and_setuptools_path += [ + (('.egg' in m.__file__) + and m.__file__[:m.__file__.rfind('.egg')+4] + or os.path.dirname(os.path.dirname(os.path.dirname(m.__file__))) + ) + for m in (zc.buildout,) + ] + +_versions = {sys.executable: '%d.%d' % sys.version_info[:2]} +def _get_version(executable): + try: + return _versions[executable] + except KeyError: + i, o = os.popen4(executable + ' -V') + i.close() + version = o.read().strip() + o.close() + pystring, version = version.split() + assert pystring == 'Python' + version = re.match('(\d[.]\d)[.]\d$', version).group(1) + _versions[executable] = version + return version + +def _satisfied(req, env): + dists = env[req.project_name] + + best = None + for dist in dists: + if (dist.precedence == pkg_resources.DEVELOP_DIST) and (dist in req): + if best is not None and best.location != dist.location: + raise ValueError('Multiple devel eggs for', req) + best = dist + + if best is not None: + return best + + specs = [(pkg_resources.parse_version(v), op) for (op, v) in req.specs] + specs.sort() + maxv = None + greater = False + lastv = None + for v, op in specs: + if op == '==' and not greater: + maxv = v + elif op in ('>', '>=', '!='): + maxv = None + greater == True + elif op == '<': + maxv = None + greater == False + elif op == '<=': + maxv = v + greater == False + + if v == lastv: + # Repeated versions values are undefined, so + # all bets are off + maxv = None + greater = True + else: + lastv = v + + if maxv is not None: + for dist in dists: + if dist.parsed_version == maxv: + return dist + + return None + + +if sys.platform == 'win32': + # work around spawn lamosity on windows + # XXX need safe quoting (see the subproces.list2cmdline) and test + def _safe_arg(arg): + return '"%s"' % arg +else: + _safe_arg = str + +_easy_install_cmd = _safe_arg( + 'from setuptools.command.easy_install import main; main()' + ) + +def _call_easy_install(spec, dest, links=(), + index = None, + executable=sys.executable, + always_unzip=False, + ): + prefix = sys.exec_prefix + os.path.sep + path = os.pathsep.join([p for p in sys.path if not p.startswith(prefix)]) + + args = ('-c', _easy_install_cmd, '-mUNxd', _safe_arg(dest)) + if links: + args += ('-f', _safe_arg(' '.join(links))) + if index: + args += ('-i', index) + if always_unzip: + args += ('-Z', ) + level = logger.getEffectiveLevel() + if level > logging.DEBUG: + args += ('-q', ) + elif level < logging.DEBUG: + args += ('-v', ) + + args += (spec, ) + + if level <= logging.DEBUG: + logger.debug('Running easy_install:\n%s "%s"\npath=%s\n', + executable, '" "'.join(args), path) + + args += (dict(os.environ, PYTHONPATH=path), ) + sys.stdout.flush() # We want any pending output first + exit_code = os.spawnle(os.P_WAIT, executable, executable, *args) + assert exit_code == 0 + + # We may overwrite distributions, so clear importer + # cache. + sys.path_importer_cache.clear() + + + +def _get_dist(requirement, env, ws, + dest, links, index, executable, always_unzip): + # Maybe an existing dist is already the best dist that satisfies the + # requirement + dist = _satisfied(requirement, env) + + # XXX Special case setuptools because: + # 1. Almost everything depends on it and + # 2. It is expensive to checl for. + # Need to think of a cleaner way to handle this. + # If we already have a satisfactory version, use it. + if dist is None and requirement.project_name == 'setuptools': + dist = env.best_match(requirement, ws) + + if dist is None: + if dest is not None: + # May need a new one. Call easy_install + _call_easy_install(str(requirement), dest, links, index, + executable, always_unzip) + + # Because we may have added new eggs, we need to rescan + # the destination directory. A possible optimization + # is to get easy_install to recod the files installed + # and either firgure out the distribution added, or + # only rescan if any files have been added. + env.scan([dest]) + + dist = env.best_match(requirement, ws) + + if dist is None: + raise ValueError("Couldn't find", requirement) + + # XXX Need test for this + if dist.has_metadata('dependency_links.txt'): + for link in dist.get_metadata_lines('dependency_links.txt'): + link = link.strip() + if link not in links: + links.append(link) + + return dist + +def install(specs, dest, + links=(), index=None, + executable=sys.executable, always_unzip=False, + path=None): + + logger.debug('Installing %r', specs) + + path = path and path[:] or [] + if dest is not None: + path.insert(0, dest) + + path += buildout_and_setuptools_path + + links = list(links) # make copy, because we may need to mutate + + + # For each spec, see if it is already installed. We create a working + # set to keep track of what we've collected and to make sue than the + # distributions assembled are consistent. + env = pkg_resources.Environment(path, python=_get_version(executable)) + requirements = [pkg_resources.Requirement.parse(spec) for spec in specs] + + ws = pkg_resources.WorkingSet([]) + + for requirement in requirements: + ws.add(_get_dist(requirement, env, ws, + dest, links, index, executable, always_unzip) + ) + + # OK, we have the requested distributions and they're in the working + # set, but they may have unmet requirements. We'll simply keep + # trying to resolve requirements, adding missing requirements as they + # are reported. + # + # Note that we don't pass in the environment, because we + # want to look for new eggs unless what we have is the best that matches + # the requirement. + while 1: + try: + ws.resolve(requirements) + except pkg_resources.DistributionNotFound, err: + [requirement] = err + if dest: + logger.debug('Getting required %s', requirement) + ws.add(_get_dist(requirement, env, ws, + dest, links, index, executable, always_unzip) + ) + else: + break + + return ws + + +def _editable(spec, dest, links=(), index = None, executable=sys.executable): + prefix = sys.exec_prefix + os.path.sep + path = os.pathsep.join([p for p in sys.path if not p.startswith(prefix)]) + args = ('-c', _easy_install_cmd, '-eb', _safe_arg(dest)) + if links: + args += ('-f', ' '.join(links)) + if index: + args += ('-i', index) + level = logger.getEffectiveLevel() + if level > logging.DEBUG: + args += ('-q', ) + elif level < logging.DEBUG: + args += ('-v', ) + + args += (spec, ) + + if level <= logging.DEBUG: + logger.debug('Running easy_install:\n%s "%s"\npath=%s\n', + executable, '" "'.join(args), path) + + args += (dict(os.environ, PYTHONPATH=path), ) + sys.stdout.flush() # We want any pending output first + exit_code = os.spawnle(os.P_WAIT, executable, executable, *args) + assert exit_code == 0 + +def build(spec, dest, build_ext, + links=(), index=None, + executable=sys.executable, + path=None): + + # XXX we're going to download and build the egg every stinking time. + # We need to not do that. + + logger.debug('Building %r', spec) + + path = path and path[:] or [] + if dest is not None: + path.insert(0, dest) + + path += buildout_and_setuptools_path + + links = list(links) # make copy, because we may need to mutate + + # For each spec, see if it is already installed. We create a working + # set to keep track of what we've collected and to make sue than the + # distributions assembled are consistent. + env = pkg_resources.Environment(path, python=_get_version(executable)) + requirement = pkg_resources.Requirement.parse(spec) + + dist = _satisfied(requirement, env) + if dist is not None: + return dist + + # Get an editable version of the package to a temporary directory: + tmp = tempfile.mkdtemp('editable') + _editable(spec, tmp, links, index, executable) + + setup_cfg = os.path.join(tmp, requirement.key, 'setup.cfg') + if not os.path.exists(setup_cfg): + f = open(setup_cfg, 'w') + f.close() + setuptools.command.setopt.edit_config(setup_cfg, dict(build_ext=build_ext)) + + # Now run easy_install for real: + _call_easy_install( + os.path.join(tmp, requirement.key), + dest, links, index, executable, True) + +def working_set(specs, executable, path): + return install(specs, None, executable=executable, path=path) + +def scripts(reqs, working_set, executable, dest, + scripts=None, + extra_paths=(), + arguments='', + ): + reqs = [pkg_resources.Requirement.parse(r) for r in reqs] + projects = [r.project_name for r in reqs] + path = [dist.location for dist in working_set] + path.extend(extra_paths) + path = repr(path)[1:-1].replace(',', ',\n ') + generated = [] + + for dist in working_set: + if dist.project_name in projects: + for name in pkg_resources.get_entry_map(dist, 'console_scripts'): + if scripts is not None: + sname = scripts.get(name) + if sname is None: + continue + else: + sname = name + + sname = os.path.join(dest, sname) + generated.extend( + _script(dist, 'console_scripts', name, path, sname, + executable, arguments) + ) + + name = 'py-'+dist.project_name + if scripts is not None: + sname = scripts.get(name) + else: + sname = name + + if sname is not None: + sname = os.path.join(dest, sname) + generated.extend( + _pyscript(path, sname, executable) + ) + + return generated + +def _script(dist, group, name, path, dest, executable, arguments): + entry_point = dist.get_entry_info(group, name) + generated = [] + if sys.platform == 'win32': + # generate exe file and give the script a magic name: + open(dest+'.exe', 'wb').write( + pkg_resources.resource_string('setuptools', 'cli.exe') + ) + generated.append(dest+'.exe') + dest += '-script.py' + + open(dest, 'w').write(script_template % dict( + python = executable, + path = path, + project = dist.project_name, + name = name, + module_name = entry_point.module_name, + attrs = '.'.join(entry_point.attrs), + arguments = arguments, + )) + try: + os.chmod(dest, 0755) + except (AttributeError, os.error): + pass + generated.append(dest) + return generated + +script_template = '''\ +#!%(python)s + +import sys +sys.path[0:0] = [ + %(path)s + ] + +import %(module_name)s + +if __name__ == '__main__': + %(module_name)s.%(attrs)s(%(arguments)s) +''' + + +def _pyscript(path, dest, executable): + generated = [] + if sys.platform == 'win32': + # generate exe file and give the script a magic name: + open(dest+'.exe', 'wb').write( + pkg_resources.resource_string('setuptools', 'cli.exe') + ) + generated.append(dest+'.exe') + dest += '-script.py' + + open(dest, 'w').write(py_script_template % dict( + python = executable, + path = path, + )) + try: + os.chmod(dest,0755) + except (AttributeError, os.error): + pass + generated.append(dest) + return generated + +py_script_template = '''\ +#!%(python)s +import sys + +sys.path[0:0] = [ + %(path)s + ] + +_interactive = True +if len(sys.argv) > 1: + import getopt + _options, _args = getopt.getopt(sys.argv[1:], 'ic:') + _interactive = False + for (_opt, _val) in _options: + if _opt == '-i': + _interactive = True + elif _opt == '-c': + exec _val + + if _args: + sys.argv[:] = _args + execfile(sys.argv[0]) + +if _interactive: + import code + code.interact(banner="", local=globals()) +''' + + + diff --git a/zc.setuptools/src/zc/buildout/easy_install.txt b/zc.setuptools/src/zc/buildout/easy_install.txt new file mode 100644 index 00000000..5b27dc39 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/easy_install.txt @@ -0,0 +1,440 @@ +Minimal Python interface to easy_install +======================================== + +The easy_install module provides a minimal interface to the setuptools +easy_install command that provides some additional semantics: + +- By default, we look for new packages *and* the packages that + they depend on. This is somewhat like (and uses) the --upgrade + option of easy_install, except that we also upgrade required + packages. + +- If the highest-revision package satisfying a specification is + already present, then we don't try to get another one. This saves a + lot of search time in the common case that packages are pegged to + specific versions. + +- If there is a develop egg that satisfies a requirement, we don't + look for additional distributions. We always give preference to + develop eggs. + +- Distutils options for building extensions can be passed. + +The easy_install module provides a method, install, for installing one +or more packages and their dependencies. The +install function takes 2 positional arguments: + +- An iterable of setuptools requirement strings for the distributions + to be installed, and + +- A destination directory to install to and to satisfy + requirements from. + +It supports a number of optional keyword arguments: + +links + a sequence of URLs, file names, or directories to look for + links to distributions, + +index + The URL of an index server, or almost any other valid URL. :) + + If not specified, the Python Package Index, + http://cheeseshop.python.org/pypi, is used. You can specify an + alternate index with this option. If you use the links option and + if the links point to the needed distributions, then the index can + be anything and will be largely ignored. In the examples, here, + we'll just point to an empty directory on our link server. This + will make our examples run a little bit faster. + +executable + A path to a Python executable. Distributions will ne installed + using this executable and will be for the matching Python version. + +path + A list of additional directories to search for locally-installed + distributions. + +always_unzip + A flag indicating that newly-downloaded distributions should be + directories even if they could be installed as zip files. + +arguments + Source code to be used to pass arguments when calling a script entry point. + +The install method returns a working set containing the distributions +needed to meet the given requirements. + +We have a link server that has a number of eggs: + + >>> print get(link_server), + <html><body> + <a href="demo-0.1-py2.3.egg">demo-0.1-py2.3.egg</a><br> + <a href="demo-0.1-py2.4.egg">demo-0.1-py2.4.egg</a><br> + <a href="demo-0.2-py2.3.egg">demo-0.2-py2.3.egg</a><br> + <a href="demo-0.2-py2.4.egg">demo-0.2-py2.4.egg</a><br> + <a href="demo-0.3-py2.3.egg">demo-0.3-py2.3.egg</a><br> + <a href="demo-0.3-py2.4.egg">demo-0.3-py2.4.egg</a><br> + <a href="demoneeded-1.0-py2.3.egg">demoneeded-1.0-py2.3.egg</a><br> + <a href="demoneeded-1.0-py2.4.egg">demoneeded-1.0-py2.4.egg</a><br> + <a href="demoneeded-1.1-py2.3.egg">demoneeded-1.1-py2.3.egg</a><br> + <a href="demoneeded-1.1-py2.4.egg">demoneeded-1.1-py2.4.egg</a><br> + <a href="extdemo-1.4.tar.gz">extdemo-1.4.tar.gz</a><br> + <a href="index/">index/</a><br> + <a href="other-1.0-py2.3.egg">other-1.0-py2.3.egg</a><br> + <a href="other-1.0-py2.4.egg">other-1.0-py2.4.egg</a><br> + </body></html> + +Let's make a directory and install the demo egg to it, using the demo: + + >>> dest = mkdtemp('sample-install') + >>> import zc.buildout.easy_install + >>> ws = zc.buildout.easy_install.install( + ... ['demo==0.2'], dest, + ... links=[link_server], index=link_server+'index/') + +We requested version 0.2 of the demo distribution to be installed into +the destination server. We specified that we should search for links +on the link server and that we should use the (empty) link server +index directory as a package index. + +The working set contains the distributions we retrieved. + + >>> for dist in ws: + ... print dist + demo 0.2 + demoneeded 1.1 + +And the actual eggs were added to the eggs directory. + + >>> ls(dest) + - demo-0.2-py2.3.egg + - demoneeded-1.1-py2.3.egg + +If we ask for the demo distribution without a version restriction, +we'll get the newer version: + + >>> ws = zc.buildout.easy_install.install( + ... ['demo'], dest, links=[link_server], index=link_server+'index/') + >>> ls(dest) + - demo-0.2-py2.3.egg + - demo-0.3-py2.3.egg + - demoneeded-1.1-py2.3.egg + +We can supply additional distributions. We can also supply +specifications for distributions that would normally be found via +dependencies. We might do this to specify a sprcific version. + + >>> ws = zc.buildout.easy_install.install( + ... ['demo', 'other', 'demoneeded==1.0'], dest, + ... links=[link_server], index=link_server+'index/') + + >>> for dist in ws: + ... print dist + demo 0.3 + other 1.0 + demoneeded 1.0 + + >>> ls(dest) + - demo-0.2-py2.3.egg + - demo-0.3-py2.3.egg + - demoneeded-1.0-py2.3.egg + - demoneeded-1.1-py2.3.egg + - other-1.0-py2.3.egg + +We can specify an alternate Python executable, and we can specify +that, when we retrieve (or create) an egg, it should be unzipped. + + >>> dest = mkdtemp('sample-install') + >>> ws = zc.buildout.easy_install.install( + ... ['demo'], dest, links=[link_server], index=link_server+'index/', + ... always_unzip=True, executable= python2_3_executable) + + >>> ls(dest) + d demo-0.3-py2.3.egg + d demoneeded-1.1-py2.3.egg + + >>> dest = mkdtemp('sample-install') + >>> ws = zc.buildout.easy_install.install( + ... ['demo'], dest, links=[link_server], index=link_server+'index/', + ... always_unzip=True, executable=python2_4_executable) + + >>> ls(dest) + d demo-0.3-py2.4.egg + d demoneeded-1.1-py2.4.egg + +Script generation +----------------- + +The easy_install module provides support for creating scripts from +eggs. It provides a function similar to setuptools except that it +provides facilities for baking a script's path into the script. This +has two advantages: + +- The eggs to be used by a script are not chosen at run time, making + startup faster and, more importantly, deterministic. + +- The script doesn't have to import pkg_resources because the logic + that pkg_resources would execute at run time is executed at + script-creation time. + +The scripts method can be used to generate scripts. Let's create a +destination directory for it to place them in: + + >>> import tempfile + >>> bin = mkdtemp() + +Now, we'll use the scripts method to generate scripts in this directory +from the demo egg: + + >>> scripts = zc.buildout.easy_install.scripts( + ... ['demo==0.1'], ws, python2_4_executable, bin) + +the four arguments we passed were: + +1. A sequence of distribution requirements. These are of the same + form as setuptools requirements. Here we passed a single + requirement, for the version 0.1 demo distribution. + +2. A working set, + +3. The Python executable to use, and + +3. The destination directory. + +The bin directory now contains 2 generated scripts: + + >>> ls(bin) + - demo + - py-demo + +The return value is a list of the scripts generated: + + >>> import os, sys + >>> if sys.platform == 'win32': + ... scripts == [os.path.join(bin, 'demo.exe'), + ... os.path.join(bin, 'demo-script.py'), + ... os.path.join(bin, 'py-demo.exe'), + ... os.path.join(bin, 'py-demo-script.py')] + ... else: + ... scripts == [os.path.join(bin, 'demo'), + ... os.path.join(bin, 'py-demo')] + True + +The demo script run the entry point defined in the demo egg: + + >>> cat(bin, 'demo') # doctest: +NORMALIZE_WHITESPACE + #!/usr/local/bin/python2.3 + <BLANKLINE> + import sys + sys.path[0:0] = [ + '/tmp/xyzsample-install/demo-0.3-py2.3.egg', + '/tmp/xyzsample-install/demoneeded-1.1-py2.3.egg' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main() + +Some things to note: + +- The demo and demoneeded eggs are added to the beginning of sys.path. + +- The module for the script entry point is imported and the entry + point, in this case, 'main', is run. + +The py-demo script simply run the Python interactive interpreter with +the path set: + + >>> cat(bin, 'py-demo') # doctest: +NORMALIZE_WHITESPACE + #!/usr/local/bin/python2.4 + import sys + <BLANKLINE> + sys.path[0:0] = [ + '/tmp/tmp5zS2Afsample-install/demo-0.3-py2.4.egg', + '/tmp/tmp5zS2Afsample-install/demoneeded-1.1-py2.4.egg' + ] + <BLANKLINE> + _interactive = True + if len(sys.argv) > 1: + import getopt + _options, _args = getopt.getopt(sys.argv[1:], 'ic:') + _interactive = False + for (_opt, _val) in _options: + if _opt == '-i': + _interactive = True + elif _opt == '-c': + exec _val + <BLANKLINE> + if _args: + sys.argv[:] = _args + execfile(sys.argv[0]) + <BLANKLINE> + if _interactive: + import code + code.interact(banner="", local=globals()) + +If invoked with a script name and arguments, it will run that script, instead. + +An additional argumnet can be passed to define which scripts to install +and to provie script names. The argument is a dictionary mapping +original script names to new script names. + + >>> bin = mkdtemp() + >>> scripts = zc.buildout.easy_install.scripts( + ... ['demo==0.1'], ws, python2_4_executable, bin, dict(demo='run')) + + >>> if sys.platform == 'win32': + ... scripts == [os.path.join(bin, 'run.exe'), + ... os.path.join(bin, 'run-script.py')] + ... else: + ... scripts == [os.path.join(bin, 'run')] + True + >>> ls(bin) + - run + + >>> print system(os.path.join(bin, 'run')), + 3 1 + +Including extra paths in scripts +-------------------------------- + +We can pass a keyword argument, extra paths, to caue additional paths +to be included in the a generated script: + + >>> scripts = zc.buildout.easy_install.scripts( + ... ['demo==0.1'], ws, python2_4_executable, bin, dict(demo='run'), + ... extra_paths=['/foo/bar']) + + >>> cat(bin, 'run') # doctest: +NORMALIZE_WHITESPACE + #!/usr/local/bin/python2.3 + <BLANKLINE> + import sys + sys.path[0:0] = [ + '/tmp/xyzsample-install/demo-0.3-py2.3.egg', + '/tmp/xyzsample-install/demoneeded-1.1-py2.3.egg', + '/foo/bar' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main() + +Providing script arguments +-------------------------- + +A n "argument" keyword argument can be used to pass arguments to an +entry point. The value passed source string to be placed between the +parentheses in the call: + + >>> scripts = zc.buildout.easy_install.scripts( + ... ['demo==0.1'], ws, python2_4_executable, bin, dict(demo='run'), + ... arguments='1, 2') + + >>> cat(bin, 'run') # doctest: +NORMALIZE_WHITESPACE + #!/usr/local/bin/python2.3 + import sys + sys.path[0:0] = [ + '/tmp/xyzsample-install/demo-0.3-py2.3.egg', + '/tmp/xyzsample-install/demoneeded-1.1-py2.3.egg' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main(1, 2) + + + +Handling custom build options for extensions +-------------------------------------------- + +Sometimes, we need to control how extension modules are built. The +build method provides this level of control. It takes a single +package specification, downloads a source distribution, and builds it +with specified custom build options. + +The build method takes 3 positional arguments: + +spec + A package specification + +dest + A destination directory + +build_ext + A dictionary of options to be passed to the distutils build_ext + command when building extensions. + +It supports a number of optional keyword arguments: + +links + a sequence of URLs, file names, or directories to look for + links to distributions, + +index + The URL of an index server, or almost any other valid URL. :) + + If not specified, the Python Package Index, + http://cheeseshop.python.org/pypi, is used. You can specify an + alternate index with this option. If you use the links option and + if the links point to the needed distributions, then the index can + be anything and will be largely ignored. In the examples, here, + we'll just point to an empty directory on our link server. This + will make our examples run a little bit faster. + +executable + A path to a Python executable. Distributions will ne installed + using this executable and will be for the matching Python version. + +path + A list of additional directories to search for locally-installed + distributions. + +always_unzip + A flag indicating that newly-downloaded distributions should be + directories even if they could be installed as zip files. + +Our link server included a source distribution that includes a simple +extension, extdemo.c:: + + #include <Python.h> + #include <extdemo.h> + + static PyMethodDef methods[] = {}; + + PyMODINIT_FUNC + initextdemo(void) + { + PyObject *d; + d = Py_InitModule3("extdemo", methods, ""); + PyDict_SetItemString(d, "val", PyInt_FromLong(EXTDEMO)); + } + +The extension depends on a system-dependnt include file, extdemo.h, +that defines a constant, EXTDEMO, that is exposed by the extension. + +We'll add an include directory to our sample buildout and add the +needed include file to it: + + >>> mkdir(sample_buildout, 'include') + >>> open(os.path.join(sample_buildout, 'include', 'extdemo.h'), 'w').write( + ... "#define EXTDEMO 42\n") + +Now, we can use the build function to create an egg from the source +distribution: + + >>> zc.buildout.easy_install.build( + ... 'extdemo', dest, + ... {'include-dirs': os.path.join(sample_buildout, 'include')}, + ... links=[link_server], index=link_server+'index/') + +Now if we look in our destination directory, we see we have an extdemo egg: + + >>> ls(dest) + d demo-0.3-py2.4.egg + d demoneeded-1.1-py2.4.egg + d extdemo-1.4-py2.3-unix-i686.egg + diff --git a/zc.setuptools/src/zc/buildout/testing.py b/zc.setuptools/src/zc/buildout/testing.py new file mode 100644 index 00000000..cf2a807f --- /dev/null +++ b/zc.setuptools/src/zc/buildout/testing.py @@ -0,0 +1,418 @@ +############################################################################## +# +# 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. +# +############################################################################## +"""Various test-support utility functions + +$Id$ +""" + + +import BaseHTTPServer, ConfigParser, os, random, re, shutil, socket, sys +import tempfile, threading, time, urllib2, unittest + +from zope.testing import doctest, renormalizing +import pkg_resources + +import zc.buildout.buildout + +def cat(dir, *names): + path = os.path.join(dir, *names) + if (not os.path.exists(path) + and sys.platform == 'win32' + and os.path.exists(path+'-script.py') + ): + path = path+'-script.py' + 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 get(url): + return urllib2.urlopen(url).read() + +def buildoutSetUp(test): + # we both need to make sure that HOME isn't set and be prepared + # to restore whatever it was after the test. + test.globs['_oldhome'] = os.environ.pop('HOME', None) + + temporary_directories = [] + def mkdtemp(*args): + d = tempfile.mkdtemp(*args) + temporary_directories.append(d) + return d + + sample = mkdtemp('sample-buildout') + + # Create a basic buildout.cfg to avoid a warning from buildout: + open(os.path.join(sample, 'buildout.cfg'), 'w').write( + "[buildout]\nparts =\n" + ) + + # Use the buildout bootstrap command to create a buildout + zc.buildout.buildout.Buildout(os.path.join(sample, 'buildout.cfg'), () + ).bootstrap([]) + + test.globs.update(dict( + __here = os.getcwd(), + sample_buildout = sample, + ls = ls, + cat = cat, + mkdir = mkdir, + write = write, + system = system, + get = get, + __temporary_directories__ = temporary_directories, + __tearDown__ = [], + mkdtemp = mkdtemp, + )) + +def buildoutTearDown(test): + os.chdir(test.globs['__here']) + for d in test.globs['__temporary_directories__']: + shutil.rmtree(d) + for f in test.globs['__tearDown__']: + f() + if test.globs.get('_oldhome') is not None: + os.environ['HOME'] = test.globs['_oldhome'] + + +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')()) +''' + +def runsetup(d, executable): + here = os.getcwd() + try: + os.chdir(d) + os.spawnle( + os.P_WAIT, executable, executable, + 'setup.py', '-q', 'bdist_egg', + {'PYTHONPATH': os.path.dirname(pkg_resources.__file__)}, + ) + shutil.rmtree('build') + finally: + os.chdir(here) + +def create_sample_eggs(test, executable=sys.executable): + if 'sample_eggs' in test.globs: + sample = os.path.dirname(test.globs['sample_eggs']) + else: + sample = test.globs['mkdtemp']('sample-eggs') + test.globs['sample_eggs'] = os.path.join(sample, 'dist') + write(sample, 'README.txt', '') + + for i in (0, 1): + write(sample, 'eggrecipedemobeeded.py', 'y=%s\n' % i) + write( + sample, 'setup.py', + "from setuptools import setup\n" + "setup(name='demoneeded', py_modules=['eggrecipedemobeeded']," + " zip_safe=True, version='1.%s')\n" + % i + ) + runsetup(sample, executable) + + write( + sample, 'setup.py', + "from setuptools import setup\n" + "setup(name='other', zip_safe=True, version='1.0', " + "py_modules=['eggrecipedemobeeded'])\n" + ) + runsetup(sample, executable) + + os.remove(os.path.join(sample, 'eggrecipedemobeeded.py')) + + for i in (1, 2, 3): + write( + sample, 'eggrecipedemo.py', + 'import eggrecipedemobeeded\n' + 'x=%s\n' + 'def main(): print x, eggrecipedemobeeded.y\n' + % i) + 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, executable) + +def find_python(version): + e = os.environ.get('PYTHON%s' % version) + if e is not None: + return e + if sys.platform == 'win32': + e = '\Python%s%s\python.exe' % tuple(version.split('.')) + if os.path.exists(e): + return e + else: + i, o = os.popen4('python%s -c "import sys; print sys.executable"' + % version) + i.close() + e = o.read().strip() + o.close() + if os.path.exists(e): + return e + i, o = os.popen4( + 'python -c "import sys; print \'%s.%s\' % sys.version_info[:2]"' + ) + i.close() + e = o.read().strip() + o.close() + if e == version: + i, o = os.popen4('python -c "import sys; print sys.executable"') + i.close() + e = o.read().strip() + o.close() + if os.path.exists(e): + return e + + raise ValueError( + "Couldn't figure out the exectable for Python %(version)s.\n" + "Set the environment variable PYTHON%(version)s to the location\n" + "of the Python %(version)s executable before running the tests." + ) + +def multi_python(test): + p23 = find_python('2.3') + p24 = find_python('2.4') + create_sample_eggs(test, executable=p23) + create_sample_eggs(test, executable=p24) + test.globs['python2_3_executable'] = p23 + test.globs['python2_4_executable'] = p24 + + + +extdemo_c = """ +#include <Python.h> +#include <extdemo.h> + +static PyMethodDef methods[] = {{NULL}}; + +PyMODINIT_FUNC +initextdemo(void) +{ + PyObject *d; + d = Py_InitModule3("extdemo", methods, ""); + PyDict_SetItemString(d, "val", PyInt_FromLong(EXTDEMO)); +} +""" + +extdemo_setup_py = """ +from distutils.core import setup, Extension + +setup(name = "extdemo", version = "1.4", url="http://www.zope.org", + author="Demo", author_email="demo@demo.com", + ext_modules = [Extension('extdemo', ['extdemo.c'])], + ) +""" + +def add_source_dist(test): + import tarfile + tmp = tempfile.mkdtemp('test-sdist') + open(os.path.join(tmp, 'extdemo.c'), 'w').write(extdemo_c); + open(os.path.join(tmp, 'setup.py'), 'w').write(extdemo_setup_py); + open(os.path.join(tmp, 'README'), 'w').write(""); + open(os.path.join(tmp, 'MANIFEST.in'), 'w').write("include *.c\n"); + here = os.getcwd() + os.chdir(tmp) + status = os.spawnl(os.P_WAIT, sys.executable, sys.executable, + os.path.join(tmp, 'setup.py'), '-q', 'sdist') + os.chdir(here) + assert status == 0 + if sys.platform == 'win32': + sname = 'extdemo-1.4.zip' + else: + sname = 'extdemo-1.4.tar.gz' + + shutil.move( + os.path.join(tmp, 'dist', sname), + os.path.join(test.globs['sample_eggs'], sname), + ) + +def make_tree(test): + sample_eggs = test.globs['sample_eggs'] + tree = dict( + [(n, open(os.path.join(sample_eggs, n), 'rb').read()) + for n in os.listdir(sample_eggs) + ]) + tree['index'] = {} + return tree + +class Server(BaseHTTPServer.HTTPServer): + + def __init__(self, tree, *args): + BaseHTTPServer.HTTPServer.__init__(self, *args) + self.tree = tree + + __run = True + def serve_forever(self): + while self.__run: + self.handle_request() + + def handle_error(self, *_): + self.__run = False + +class Handler(BaseHTTPServer.BaseHTTPRequestHandler): + + def __init__(self, request, address, server): + self.tree = server.tree + BaseHTTPServer.BaseHTTPRequestHandler.__init__( + self, request, address, server) + + def do_GET(self): + if '__stop__' in self.path: + raise SystemExit + + tree = self.tree + for name in self.path.split('/'): + if not name: + continue + tree = tree.get(name) + if tree is None: + self.send_response(404, 'Not Found') + out = '<html><body>Not Found</body></html>' + self.send_header('Content-Length', str(len(out))) + self.send_header('Content-Type', 'text/html') + self.end_headers() + self.wfile.write(out) + return + + self.send_response(200) + if isinstance(tree, dict): + out = ['<html><body>\n'] + items = tree.items() + items.sort() + for name, v in items: + if isinstance(v, dict): + name += '/' + out.append('<a href="%s">%s</a><br>\n' % (name, name)) + out.append('</body></html>\n') + out = ''.join(out) + self.send_header('Content-Length', str(len(out))) + self.send_header('Content-Type', 'text/html') + else: + out = tree + self.send_header('Content-Length', len(out)) + if name.endswith('.egg'): + self.send_header('Content-Type', 'application/zip') + elif name.endswith('.gz'): + self.send_header('Content-Type', 'application/x-gzip') + elif name.endswith('.zip'): + self.send_header('Content-Type', 'application/x-gzip') + else: + self.send_header('Content-Type', 'text/html') + self.end_headers() + + self.wfile.write(out) + + def log_request(*s): + pass + +def _run(tree, port): + server_address = ('localhost', port) + httpd = Server(tree, server_address, Handler) + httpd.serve_forever() + +def get_port(): + for i in range(10): + port = random.randrange(20000, 30000) + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + try: + s.connect(('localhost', port)) + except socket.error: + return port + finally: + s.close() + raise RuntimeError, "Can't find port" + +def _start_server(tree, name=''): + port = get_port() + thread = threading.Thread(target=_run, args=(tree, port), name=name) + thread.setDaemon(True) + thread.start() + wait(port, up=True) + return port, thread + +def start_server(tree): + return _start_server(tree)[0] + +def stop_server(url, thread=None): + try: + urllib2.urlopen(url+'__stop__') + except Exception: + pass + if thread is not None: + thread.join() # wait for thread to stop + +def setUpServer(test, tree): + port, thread = _start_server(tree, name=test.name) + link_server = 'http://localhost:%s/' % port + test.globs['link_server'] = link_server + test.globs['__tearDown__'].append(lambda: stop_server(link_server, thread)) + + +def wait(port, up): + addr = 'localhost', port + for i in range(120): + time.sleep(0.25) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(addr) + s.close() + if up: + break + except socket.error, e: + if e[0] not in (errno.ECONNREFUSED, errno.ECONNRESET): + raise + s.close() + if not up: + break + else: + if up: + raise + else: + raise SystemError("Couln't stop server") diff --git a/zc.setuptools/src/zc/buildout/tests.py b/zc.setuptools/src/zc/buildout/tests.py new file mode 100644 index 00000000..0c4fbea8 --- /dev/null +++ b/zc.setuptools/src/zc/buildout/tests.py @@ -0,0 +1,343 @@ +############################################################################## +# +# 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, unittest +from zope.testing import doctest, renormalizing +import zc.buildout.testing + +os_path_sep = os.path.sep +if os_path_sep == '\\': + os_path_sep *= 2 + +def buildout_error_handling(): + r"""Buildout error handling + +Asking for a section that doesn't exist, yields a key error: + + >>> import os + >>> os.chdir(sample_buildout) + >>> import zc.buildout.buildout + >>> buildout = zc.buildout.buildout.Buildout('buildout.cfg', []) + >>> buildout['eek'] + Traceback (most recent call last): + ... + KeyError: 'eek' + +Asking for an option that doesn't exist, a MissingOption error is raised: + + >>> buildout['buildout']['eek'] + Traceback (most recent call last): + ... + MissingOption: Missing option: buildout:eek + +It is an error to create a variable-reference cycle: + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... x = ${buildout:y} + ... y = ${buildout:z} + ... z = ${buildout:x} + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + ... # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS + Error: Circular reference in substitutions. + We're evaluating buildout:y, buildout:z, buildout:x + and are referencing: buildout:y. + +It is an error to use funny characters in variable refereces: + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... x = ${bui$ldout:y} + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: The section name in substitution, ${bui$ldout:y}, + has invalid characters. + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... x = ${buildout:y{z} + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: The option name in substitution, ${buildout:y{z}, + has invalid characters. + +and too have too many or too few colons: + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... x = ${parts} + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: The substitution, ${parts}, + doesn't contain a colon. + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = data_dir debug + ... x = ${buildout:y:z} + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: The substitution, ${buildout:y:z}, + has too many colons. + +Al parts have to have a section: + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... parts = x + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: No section was specified for part x + +and all parts have to have a specified recipe: + + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... parts = x + ... + ... [x] + ... foo = 1 + ... ''') + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + Error: Missing option: x:recipe + +""" + +def test_comparing_saved_options_with_funny_characters(): + """ + If an option has newlines, extra/odd spaces or a %, we need to make + sure the comparison with the saved value works correctly. + + >>> mkdir(sample_buildout, 'recipes') + >>> write(sample_buildout, 'recipes', 'debug.py', + ... ''' + ... class Debug: + ... def __init__(self, buildout, name, options): + ... options['debug'] = \"\"\" <zodb> + ... + ... <filestorage> + ... path foo + ... </filestorage> + ... + ... </zodb> + ... \"\"\" + ... options['debug1'] = \"\"\" + ... <zodb> + ... + ... <filestorage> + ... path foo + ... </filestorage> + ... + ... </zodb> + ... \"\"\" + ... options['debug2'] = ' x ' + ... options['debug3'] = '42' + ... options['format'] = '%3d' + ... + ... def install(self): + ... open('t', 'w').write('t') + ... return 't' + ... ''') + + + >>> write(sample_buildout, 'recipes', 'setup.py', + ... ''' + ... from setuptools import setup + ... setup( + ... name = "recipes", + ... entry_points = {'zc.buildout': ['default = debug:Debug']}, + ... ) + ... ''') + + >>> write(sample_buildout, 'recipes', 'README.txt', " ") + + >>> write(sample_buildout, 'buildout.cfg', + ... ''' + ... [buildout] + ... develop = recipes + ... parts = debug + ... + ... [debug] + ... recipe = recipes + ... ''') + + >>> os.chdir(sample_buildout) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + + >>> print system(buildout+' -v'), # doctest: +ELLIPSIS + buildout: Running ...setup.py -q develop ... + buildout: Installing debug + +If we run the buildout again, we shoudn't get a message about +uninstalling anything because the configuration hasn't changed. + + >>> print system(buildout+' -v'), + buildout: Running setup.py -q develop ... + buildout: Installing debug +""" + +def linkerSetUp(test): + zc.buildout.testing.buildoutSetUp(test, clear_home=False) + zc.buildout.testing.multi_python(test) + zc.buildout.testing.setUpServer(test, zc.buildout.testing.make_tree(test)) + +def easy_install_SetUp(test): + zc.buildout.testing.buildoutSetUp(test) + zc.buildout.testing.multi_python(test) + zc.buildout.testing.add_source_dist(test) + zc.buildout.testing.setUpServer(test, zc.buildout.testing.make_tree(test)) + +class PythonNormalizing(renormalizing.RENormalizing): + + def _transform(self, want, got): + if '/xyzsample-install/' in want: + got = got.replace('-py2.4.egg', '-py2.3.egg') + firstg = got.split('\n')[0] + firstw = want.split('\n')[0] + if firstg.startswith('#!') and firstw.startswith('#!'): + firstg = ' '.join(firstg.split()[1:]) + got = firstg + '\n' + '\n'.join(got.split('\n')[1:]) + firstw = ' '.join(firstw.split()[1:]) + want = firstw + '\n' + '\n'.join(want.split('\n')[1:]) + + for pattern, repl in self.patterns: + want = pattern.sub(repl, want) + got = pattern.sub(repl, got) + + return want, got + + def check_output(self, want, got, optionflags): + if got == want: + return True + + want, got = self._transform(want, got) + if got == want: + return True + + return doctest.OutputChecker.check_output(self, want, got, optionflags) + + def output_difference(self, example, got, optionflags): + + want = example.want + + # If want is empty, use original outputter. This is useful + # when setting up tests for the first time. In that case, we + # generally use the differencer to display output, which we evaluate + # by hand. + if not want.strip(): + return doctest.OutputChecker.output_difference( + self, example, got, optionflags) + + # Dang, this isn't as easy to override as we might wish + original = want + want, got = self._transform(want, got) + + # temporarily hack example with normalized want: + example.want = want + result = doctest.OutputChecker.output_difference( + self, example, got, optionflags) + example.want = original + + return result + + +def test_suite(): + return unittest.TestSuite(( + doctest.DocFileSuite( + 'buildout.txt', + setUp=zc.buildout.testing.buildoutSetUp, + tearDown=zc.buildout.testing.buildoutTearDown, + checker=renormalizing.RENormalizing([ + (re.compile('__buildout_signature__ = recipes-\S+'), + '__buildout_signature__ = recipes-SSSSSSSSSSS'), + (re.compile('\S+sample-(\w+)%s(\S+)' % os_path_sep), + r'/sample-\1/\2'), + (re.compile('\S+sample-(\w+)'), r'/sample-\1'), + (re.compile('executable = \S+python\S*'), + 'executable = python'), + (re.compile('setuptools-\S+[.]egg'), 'setuptools.egg'), + (re.compile('zc.buildout(-\S+)?[.]egg(-link)?'), + 'zc.buildout.egg'), + (re.compile('creating \S*setup.cfg'), 'creating setup.cfg'), + (re.compile('(\n?)- ([a-zA-Z_.-]+)-script.py\n- \\2.exe\n'), + '\\1- \\2\n'), + (re.compile("(\w)%s(\w)" % os_path_sep), r"\1/\2"), + ]) + ), + + doctest.DocFileSuite( + 'easy_install.txt', + setUp=easy_install_SetUp, + tearDown=zc.buildout.testing.buildoutTearDown, + + checker=PythonNormalizing([ + (re.compile("'" + "(\w:)?" + "[%(sep)s/]\S+sample-install[%(sep)s/]" + "[%(sep)s/]?(dist" + "[%(sep)s/])?" + % dict(sep=os_path_sep)), + '/sample-eggs/'), + (re.compile("([d-] ((ext)?demo(needed)?|other)" + "-\d[.]\d-py)\d[.]\d(-[^. \t\n]+)?[.]egg"), + '\\1V.V.egg'), + (re.compile('(\n?)- ([a-zA-Z_.-]+)-script.py\n- \\2.exe\n'), + '\\1- \\2\n'), + (re.compile('extdemo-1[.]4[.]tar[.]gz'), 'extdemo-1.4.zip'), + (re.compile('#!\S+python\S+'), '#!python'), + ]), + ), + doctest.DocTestSuite( + setUp=zc.buildout.testing.buildoutSetUp, + tearDown=zc.buildout.testing.buildoutTearDown, + + checker=PythonNormalizing([ + (re.compile("buildout: Running \S*setup.py"), + 'buildout: Running setup.py'), + ]), + ) + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff --git a/zc.setuptools/todo.txt b/zc.setuptools/todo.txt new file mode 100644 index 00000000..36e74455 --- /dev/null +++ b/zc.setuptools/todo.txt @@ -0,0 +1,66 @@ + +- Use setuptools PackageIndex objects to improve performance by + deciding whether we need to download anything without using + easy_install. + +- tests + + - distribution dependency links + + - offline mode (there is an indirect test in the testrunner tests) + + +- Load from urls + +- control python for develop (probbaly a new recipe) + +- proper handling of extras + +- Common recipes + + - configure-make-make-install + + - download, checkout + + - Should it be possible to provide multiple recipies? + Or should recipies be combined through inheritence (or + composition)? + + - Python + +- Some way to freeze versions so we can have reproducable buildouts. + + Maybe simple approach: + + - Egg recipe outputs dependency info with debug logging + + - Egg recipe has option to specify dependencies. When used, + don't automatically fetch newer data. + +- Option to search python path for distros + +- Part dependencies + +- custom uninstall + +- Fix develop so thet ordinary eggs fetched as dependencies end up + in eggs directory. + + "fixed" except that fix is ineffective due to setuptools bug. :( + + +- spelling :) + +- document recipe initialization order + + + + +Issues + +- Should we include setuptools and buildout eggs for buildout process + in environment when searching for requirements? + +- We don't want to look for new versions of setuptools all the time. + For now, we always use a local dist if there is one. Needs more + thought. diff --git a/zc.setuptools/zc.recipe.egg_/README.txt b/zc.setuptools/zc.recipe.egg_/README.txt new file mode 100644 index 00000000..b01c7a7c --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/README.txt @@ -0,0 +1,69 @@ +Buildout Egg-Installation Recipe +================================ + +The egg-installation recipe installes eggs into a buildout eggs +directory. It also generates scripts in a buildout bin directory with +egg paths baked into them. + +The recipe provides the following options: + +eggs + A list of eggs to install given as one ore more setuptools + requirement strings. Each string must be given on a separate + line. + +find-links + One or more addresses of link servers to be searched for + distributions. This is optional. If not specified, links + specified in the buildout section will be used, if any. + +index + The optional address of a distribution index server. If not + specified, then the option from the buildout section will be + used. If not specified in the part data or in the buildout + section, then http://www.python.org/pypi is used. + +python + The name of a section defining the Python executable to use. + This defaults to buildout. + +scripts + Control which scripts are generated. The value should be a list of + zero or more tokens. Each token is either a name, or a name + followed by an '=' and a new name. Only the named scripts are + generated. If no tokens are given, then script generation is + disabled. If the option isn't given at all, then all scripts + defined by the named eggs will be generated. + +Custom eggs +----------- + +The zc.recipe.egg:custom recipe supports building custom eggs, +currently with specialized options for building extensions. + +extra-paths + Extra paths to include in a generates script. + +To do +----- + +- Some way to freeze the egg-versions used. This includes some way to + record which versions were selected dynamially and then a way to + require that the recorded versions be used in a later run. + +- More control over script generation. In particular, some way to + specify data t be recored in the script. + +Change History +============== + +1.0.0a2 +------- + +Added a new recipe for building custom eggs from source distributions, +specifying custom distutils build_ext options. + +1.0.0a1 +------- + +Initial public version diff --git a/zc.setuptools/zc.recipe.egg_/setup.py b/zc.setuptools/zc.recipe.egg_/setup.py new file mode 100644 index 00000000..c9fafb57 --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup, find_packages + +name = "zc.recipe.egg" +setup( + name = name, + version = "1.0.0a2", + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "Recipe for installing Python package distributions as eggs", + long_description = open('README.txt').read(), + license = "ZPL 2.1", + keywords = "development build", + url='http://svn.zope.org/zc.buildout', + + packages = find_packages('src'), + include_package_data = True, + package_dir = {'':'src'}, + namespace_packages = ['zc', 'zc.recipe'], + install_requires = ['zc.buildout', 'setuptools'], + tests_require = ['zope.testing'], + test_suite = name+'.tests.test_suite', + entry_points = {'zc.buildout': ['default = %s:Egg' % name, + 'custom = %s:Custom' % name, + ] + }, + dependency_links = ['http://download.zope.org/distribution/'], + ) diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/__init__.py b/zc.setuptools/zc.recipe.egg_/src/zc/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/__init__.py b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/README.txt b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/README.txt new file mode 100644 index 00000000..c73501cd --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/README.txt @@ -0,0 +1,269 @@ +Installation of distributions as eggs +===================================== + +The zc.recipe.egg recipe can be used to install various types if +distutils distributions as eggs. It takes a number of options: + +eggs + A list of eggs to install given as one ore more setuptools + requirement strings. Each string must be given on a separate + line. + +find-links + A list of URLs, files, or directories to search for distributions. + +index + The URL of an index server, or almost any other valid URL. :) + + If not specified, the Python Package Index, + http://cheeseshop.python.org/pypi, is used. You can specify an + alternate index with this option. If you use the links option and + if the links point to the needed distributions, then the index can + be anything and will be largely ignored. In the examples, here, + we'll just point to an empty directory on our link server. This + will make our examples run a little bit faster. + +python + The name of a section to get the Python executable from. + If not specified, then the buildout python option is used. The + Python executable is found in the executable option of the named + section. + +scripts + Control which scripts are generated. The value should be a list of + zero or more tokens. Each token is either a name, or a name + followed by an '=' and a new name. Only the named scripts are + generated. If no tokens are given, then script generation is + disabled. If the option isn't given at all, then all scripts + defined by the named eggs will be generated. + +extra-paths + Extra paths to include in a generates script. + +We have a link server that has a number of eggs: + + >>> print get(link_server), + <html><body> + <a href="demo-0.1-py2.3.egg">demo-0.1-py2.3.egg</a><br> + <a href="demo-0.2-py2.3.egg">demo-0.2-py2.3.egg</a><br> + <a href="demo-0.3-py2.3.egg">demo-0.3-py2.3.egg</a><br> + <a href="demoneeded-1.0-py2.3.egg">demoneeded-1.0-py2.3.egg</a><br> + <a href="demoneeded-1.1-py2.3.egg">demoneeded-1.1-py2.3.egg</a><br> + <a href="index/">index/</a><br> + <a href="other-1.0-py2.3.egg">other-1.0-py2.3.egg</a><br> + </body></html> + + +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 + ... eggs = demo<0.3 + ... find-links = %(server)s + ... index = %(server)s/index + ... """ % dict(server=link_server)) + +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) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(buildout), + +Now, if we look at the buildout eggs directory: + + >>> ls(sample_buildout, 'eggs') + - demo-0.2-py2.3.egg + - demoneeded-1.1-py2.3.egg + - setuptools-0.6-py2.3.egg + - zc.buildout-1.0-py2.3.egg + +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 + - py-zc.buildout + +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 debugging and 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'), + ... """import os, sys + ... for p in sys.path: + ... if 'demo' in p: + ... print os.path.basename(p) + ... + ... """).replace('>>> ', '').replace('... ', ''), + ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + demo-0.2-py2.4.egg + demoneeded-1.1-py2.4.egg + +The recipe gets the most recent distribution that satisfies the +specification. For example, We remove the restriction on demo: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... + ... [demo] + ... recipe = zc.recipe.egg + ... find-links = %(server)s + ... index = %(server)s/index + ... """ % dict(server=link_server)) + + >>> print system(buildout), + +Then we'll get a new demo egg: + + >>> ls(sample_buildout, 'eggs') + - demo-0.2-py2.3.egg + - demo-0.3-py2.3.egg + - demoneeded-1.0-py2.3.egg + - setuptools-0.6-py2.4.egg + - zc.buildout-1.0-py2.4.egg + +Note that we removed the eggs option, and the eggs +defaulted to the part name. + +The script is updated too: + + >>> print system(os.path.join(sample_buildout, 'bin', 'demo')), + 3 1 + +You can control which scripts get generated using the scripts option. +For example, to suppress scripts, use the scripts option without any +arguments: + + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... + ... [demo] + ... recipe = zc.recipe.egg + ... find-links = %(server)s + ... index = %(server)s/index + ... scripts = + ... """ % dict(server=link_server)) + + + >>> print system(buildout), + + >>> ls(sample_buildout, 'bin') + - buildout + - py-zc.buildout + +You can also control the name used for scripts: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... + ... [demo] + ... recipe = zc.recipe.egg + ... find-links = %(server)s + ... index = %(server)s/index + ... scripts = demo=foo + ... """ % dict(server=link_server)) + + >>> print system(buildout), + + >>> ls(sample_buildout, 'bin') + - buildout + - foo + - py-zc.buildout + +If we need to include extra paths in a script, we can use the +extra-paths option: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... + ... [demo] + ... recipe = zc.recipe.egg + ... find-links = %(server)s + ... index = %(server)s/index + ... scripts = demo=foo + ... extra-paths = + ... /foo/bar + ... /spam/eggs + ... """ % dict(server=link_server)) + + >>> print system(buildout), + +Let's look at the script that was generated: + + >>> cat(sample_buildout, 'bin', 'foo') # doctest: +NORMALIZE_WHITESPACE + #!/usr/local/bin/python2.3 + <BLANKLINE> + import sys + sys.path[0:0] = [ + '/tmp/xyzsample-install/demo-0.3-py2.3.egg', + '/tmp/xyzsample-install/demoneeded-1.1-py2.3.egg', + '/foo/bar', + '/spam/eggs' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main() + + + +Offline mode +------------ + +If the buildout offline option is set to "true", then no attempt will +be made to contact an index server: + + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... offline = true + ... + ... [demo] + ... recipe = zc.recipe.egg + ... index = eek! + ... scripts = demo=foo + ... """ % dict(server=link_server)) + + >>> print system(buildout), diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/__init__.py b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/__init__.py new file mode 100644 index 00000000..49ac3db8 --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/__init__.py @@ -0,0 +1,2 @@ +from zc.recipe.egg.egg import Egg +from zc.recipe.egg.custom import Custom diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/api.txt b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/api.txt new file mode 100644 index 00000000..84ea119c --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/api.txt @@ -0,0 +1,110 @@ +Egg Recipe API for other Recipes +================================ + +It is common for recipes to accept a collection of egg specifications +and generate scripts based on the resulting working sets. The egg +recipe provides an API that other recipes can use. + +A recipe can reuse the egg recipe, supporting the eggs, find-links, +index, and python options. This is done by creating an egg recipe +instance in a recipes's contructor. In the recipe's install script, +the egg-recipe instance's working_set method is used to collect the +requested eggs and working set. + +To illustrate, we create a sample recipe that is a very thin layer +around the egg recipe: + + >>> mkdir(sample_buildout, 'sample') + >>> write(sample_buildout, 'sample', 'sample.py', + ... """ + ... import logging, os + ... import zc.recipe.egg + ... + ... class Sample: + ... + ... def __init__(self, buildout, name, options): + ... self.egg = zc.recipe.egg.Egg(buildout, name, options) + ... self.name = name + ... self.options = options + ... + ... def install(self): + ... extras = self.options['extras'].split() + ... requirements, ws = self.egg.working_set(extras) + ... print 'Part:', self.name + ... print 'Egg requirements:' + ... for r in requirements: + ... print r + ... print 'Working set:' + ... for d in ws: + ... print d + ... """) + +Here we instantiated the egg recipe in the constructor, saving it in +an attribute. This also initialized the options dictionary. + +In our install method, we called the working_set method on the +instance we saved. The working_set method takes an optional sequence +of extra requirements to be included in the working set. + + >>> write(sample_buildout, 'sample', 'setup.py', + ... """ + ... from setuptools import setup + ... + ... setup( + ... name = "sample", + ... entry_points = {'zc.buildout': ['default = sample:Sample']}, + ... install_requires = 'zc.recipe.egg', + ... ) + ... """) + + + >>> write(sample_buildout, 'sample', 'README.txt', " ") + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = sample + ... parts = sample-part + ... + ... [sample-part] + ... recipe = sample + ... eggs = demo<0.3 + ... find-links = %(server)s + ... index = %(server)sindex + ... extras = other + ... """ % dict(server=link_server)) + + >>> import os + >>> os.chdir(sample_buildout) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(buildout), + Part: sample-part + Egg requirements: + demo<0.3 + Working set: + demo 0.2 + other 1.0 + demoneeded 1.1 + +We can see that the options were augmented with additionl data +computed by the egg recipe by looking at .installed.cfg: + + >>> cat(sample_buildout, '.installed.cfg') + [buildout] + parts = sample-part + <BLANKLINE> + [sample-part] + __buildout_installed__ = + __buildout_signature__ = sample-6aWMvV2EJ9Ijq+bR8ugArQ== + zc.recipe.egg-cAsnudgkduAa/Fd+WJIM6Q== + setuptools-0.6-py2.4.egg + zc.buildout-+rYeCcmFuD1K/aB77XTj5A== + _b = /tmp/tmpb7kP9bsample-buildout/bin + _d = /tmp/tmpb7kP9bsample-buildout/develop-eggs + _e = /tmp/tmpb7kP9bsample-buildout/eggs + eggs = demo<0.3 + executable = /usr/local/bin/python2.3 + extras = other + find-links = http://localhost:27071/ + index = http://localhost:27071/index + recipe = sample diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.py b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.py new file mode 100644 index 00000000..da202afe --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.py @@ -0,0 +1,82 @@ +############################################################################## +# +# 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 os, re, zipfile +import zc.buildout.easy_install + +class Custom: + + def __init__(self, buildout, name, options): + self.buildout = buildout + self.name = name + self.options = options + links = options.get('find-links', + buildout['buildout'].get('find-links')) + if links: + links = links.split() + options['find-links'] = '\n'.join(links) + else: + links = () + self.links = links + + index = options.get('index', buildout['buildout'].get('index')) + if index is not None: + options['index'] = index + self.index = index + + options['_b'] = buildout['buildout']['bin-directory'] + options['_e'] = buildout['buildout']['eggs-directory'] + options['_d'] = buildout['buildout']['develop-eggs-directory'] + + assert options.get('unzip') in ('true', 'false', None) + + python = options.get('python', buildout['buildout']['python']) + options['executable'] = buildout[python]['executable'] + + build_ext = {} + for be_option in ('include-dirs', 'library-dirs', 'rpath'): + value = options.get(be_option) + if value is None: + continue + value = [ + os.path.join( + buildout['buildout']['directory'], + v.strip() + ) + for v in value.strip().split('\n') + if v.strip() + ] + build_ext[be_option] = ':'.join(value) + options[be_option] = ':'.join(value) + self.build_ext = build_ext + + def install(self): + if self.buildout['buildout'].get('offline') == 'true': + return + options = self.options + distribution = options.get('eggs', self.name).strip() + build_ext = dict([ + (k, options[k]) + for k in ('include-dirs', 'library-dirs', 'rpath') + if k in options + ]) + zc.buildout.easy_install.build( + distribution, options['_d'], self.build_ext, + self.links, self.index, options['executable'], [options['_e']], + ) + diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.txt b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.txt new file mode 100644 index 00000000..e1202513 --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/custom.txt @@ -0,0 +1,114 @@ +Custon eggs +=========== + +Sometimes, It's necessary to provide extra control over how an egg is +created. This is commonly true for eggs with extension modules that +need to access libraries or include files. + +The zc.recipe.egg:custom recipe can be used to define an egg with +custom build parameters. The currently defined parameters are: + +include-dirs + A new-line separated list of directories to search for include + files. + +library-dirs + A new-line separated list of directories to search for libraries + to link with. + +rpath + A new-line separated list of directories to search for dynamic libraries + at run time. + +In addition, the following options can be used to specify the egg: + +egg + An eggs to install given as a setuptools requirement string. + This defaults to the part name. + +find-links + A list of URLs, files, or directories to search for distributions. + +index + The URL of an index server, or almost any other valid URL. :) + + If not specified, the Python Package Index, + http://cheeseshop.python.org/pypi, is used. You can specify an + alternate index with this option. If you use the links option and + if the links point to the needed distributions, then the index can + be anything and will be largely ignored. In the examples, here, + we'll just point to an empty directory on our link server. This + will make our examples run a little bit faster. + +python + The name of a section to get the Python executable from. + If not specified, then the buildout python option is used. The + Python executable is found in the executable option of the named + section. + +To illustrate this, we'll define a buildout that builds an egg for a +package that has a simple extension module:: + + #include <Python.h> + #include <extdemo.h> + + static PyMethodDef methods[] = {}; + + PyMODINIT_FUNC + initextdemo(void) + { + PyObject *d; + d = Py_InitModule3("extdemo", methods, ""); + PyDict_SetItemString(d, "val", PyInt_FromLong(EXTDEMO)); + } + +The extension depends on a system-dependnt include file, extdemo.h, +that defines a constant, EXTDEMO, that is exposed by the extension. + +The extension module is available as a source distribution, +extdemo-1.4.tar.gz, on a distribution server. + +We have a sample buildout that we'll add an include directory to with +the necessary include file: + + >>> mkdir(sample_buildout, 'include') + >>> import os + >>> open(os.path.join(sample_buildout, 'include', 'extdemo.h'), 'w').write( + ... "#define EXTDEMO 42\n") + +We'll also update the buildout configuration file to define a part for +the egg: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = extdemo + ... + ... [extdemo] + ... recipe = zc.recipe.egg:custom + ... find-links = %(server)s + ... index = %(server)s/index + ... include-dirs = include + ... """ % dict(server=link_server)) + + >>> os.chdir(sample_buildout) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + + >>> print system(buildout), + zip_safe flag not set; analyzing archive contents... + +We got the zip_safe warning because the source distribution we used +wasn't setuptools based and thus didn't set the option. + +The egg is created in the develop-eggs directory *not* the eggs +directory because it depends on buildout-specific parameters and the +eggs directory can be shared across multiple buildouts. + + >>> ls(sample_buildout, 'develop-eggs') + d extdemo-1.4-py2.4-unix-i686.egg + - zc.recipe.egg.egg-link + +Note that no scripts or dependencies are installed. To install +dependencies or scripts for a custom egg, define another part and use +the zc.recipe.egg recipe, listing the custom egg as one of the eggs to +be installed. The zc.recipe.egg recipe will use the installed egg. diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/egg.py b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/egg.py new file mode 100644 index 00000000..9a47ab8f --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/egg.py @@ -0,0 +1,107 @@ +############################################################################## +# +# 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 os, re, zipfile +import zc.buildout.easy_install + +class Egg: + + def __init__(self, buildout, name, options): + self.buildout = buildout + self.name = name + self.options = options + links = options.get('find-links', + buildout['buildout'].get('find-links')) + if links: + links = links.split() + options['find-links'] = '\n'.join(links) + else: + links = () + self.links = links + + index = options.get('index', buildout['buildout'].get('index')) + if index is not None: + options['index'] = index + self.index = index + + self.extra_paths = [ + os.path.join(buildout['buildout']['directory'], p.strip()) + for p in options.get('extra-paths', '').split('\n') + if p.strip() + ] + if self.extra_paths: + options['extra-paths'] = '\n'.join(self.extra_paths) + + options['_b'] = buildout['buildout']['bin-directory'] + options['_e'] = buildout['buildout']['eggs-directory'] + options['_d'] = buildout['buildout']['develop-eggs-directory'] + + assert options.get('unzip') in ('true', 'false', None) + + python = options.get('python', buildout['buildout']['python']) + options['executable'] = buildout[python]['executable'] + + def working_set(self, extra=()): + """Separate method to just get the working set + + This is intended for reuse by similar recipes. + """ + options = self.options + + distributions = [ + r.strip() + for r in options.get('eggs', self.name).split('\n') + if r.strip()] + orig_distributions = distributions[:] + distributions.extend(extra) + + if self.buildout['buildout'].get('offline') == 'true': + ws = zc.buildout.easy_install.working_set( + distributions, options['executable'], + [options['_d'], options['_e']] + ) + else: + ws = zc.buildout.easy_install.install( + distributions, options['_e'], + links = self.links, + index = self.index, + executable = options['executable'], + always_unzip=options.get('unzip') == 'true', + path=[options['_d']] + ) + + return orig_distributions, ws + + def install(self): + distributions, ws = self.working_set() + options = self.options + + scripts = options.get('scripts') + if scripts or scripts is None: + if scripts is not None: + scripts = scripts.split() + scripts = dict([ + ('=' in s) and s.split('=', 1) or (s, s) + for s in scripts + ]) + return zc.buildout.easy_install.scripts( + distributions, ws, options['executable'], + options['_b'], + scripts=scripts, + extra_paths=self.extra_paths) + diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/selecting-python.txt b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/selecting-python.txt new file mode 100644 index 00000000..0f7ad521 --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/selecting-python.txt @@ -0,0 +1,208 @@ +Controlling which Python to use +------------------------------- + +The following assumes that your $HOME/.buildout/default.cfg has +python2.3 and python2.4 sections that define Python 2.3 and Python 2.4 +executables. + +We can specify the python to use by specifying the name of a section +to read the Python executable from. The default is the section +defined by the python buildout option. + +We have a link server: + + >>> print get(link_server), + <html><body> + <a href="demo-0.1-py2.3.egg">demo-0.1-py2.3.egg</a><br> + <a href="demo-0.1-py2.4.egg">demo-0.1-py2.4.egg</a><br> + <a href="demo-0.2-py2.3.egg">demo-0.2-py2.3.egg</a><br> + <a href="demo-0.2-py2.4.egg">demo-0.2-py2.4.egg</a><br> + <a href="demo-0.3-py2.3.egg">demo-0.3-py2.3.egg</a><br> + <a href="demo-0.3-py2.4.egg">demo-0.3-py2.4.egg</a><br> + <a href="demoneeded-1.0-py2.3.egg">demoneeded-1.0-py2.3.egg</a><br> + <a href="demoneeded-1.0-py2.4.egg">demoneeded-1.0-py2.4.egg</a><br> + <a href="demoneeded-1.1-py2.3.egg">demoneeded-1.1-py2.3.egg</a><br> + <a href="demoneeded-1.1-py2.4.egg">demoneeded-1.1-py2.4.egg</a><br> + <a href="index/">index/</a><br> + <a href="other-1.0-py2.3.egg">other-1.0-py2.3.egg</a><br> + <a href="other-1.0-py2.4.egg">other-1.0-py2.4.egg</a><br> + </body></html> + +We have a sample buildout. Let's update it's configuration file to +install the demo package using Python 2.3. + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... eggs-directory = eggs + ... + ... [python2.3] + ... executable = %(python23)s + ... + ... [demo] + ... recipe = zc.recipe.egg + ... eggs = demo <0.3 + ... find-links = %(server)s + ... index = %(server)s/index + ... python = python2.3 + ... """ % dict(server=link_server, python23=python2_3_executable)) + +Now, if we run the buildout: + + >>> import os + >>> os.chdir(sample_buildout) + >>> buildout = os.path.join(sample_buildout, 'bin', 'buildout') + >>> print system(buildout), + +we'll get the Python 2.3 eggs for demo and demoneeded: + + >>> ls(sample_buildout, 'eggs') + - demo-0.2-py2.3.egg + - demoneeded-1.1-py2.3.egg + - setuptools-0.6-py2.4.egg + - zc.buildout-1.0-py2.4.egg + +And the generated scripts invoke Python 2.3: + + >>> import sys + >>> if sys.platform == 'win32': + ... script_name = 'demo-script.py' + ... else: + ... script_name = 'demo' + >>> f = open(os.path.join(sample_buildout, 'bin', script_name)) + >>> f.readline().strip() == '#!' + python2_3_executable + True + >>> print f.read(), # doctest: +NORMALIZE_WHITESPACE + <BLANKLINE> + import sys + sys.path[0:0] = [ + '/private/tmp/tmpOEtRO8sample-buildout/eggs/demo-0.2-py2.3.egg', + '/private/tmp/tmpOEtRO8sample-buildout/eggs/demoneeded-1.1-py2.3.egg' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main() + + >>> if sys.platform == 'win32': + ... f = open(os.path.join(sample_buildout, 'bin', 'py-demo-script.py')) + ... else: + ... f = open(os.path.join(sample_buildout, 'bin', 'py-demo')) + >>> f.readline().strip() == '#!' + python2_3_executable + True + >>> print f.read(), # doctest: +NORMALIZE_WHITESPACE + import sys + <BLANKLINE> + sys.path[0:0] = [ + '/tmp/tmp5zS2Afsample-buildout/eggs/demo-0.2-py2.3.egg', + '/tmp/tmp5zS2Afsample-buildout/eggs/demoneeded-1.1-py2.3.egg' + ] + <BLANKLINE> + _interactive = True + if len(sys.argv) > 1: + import getopt + _options, _args = getopt.getopt(sys.argv[1:], 'ic:') + _interactive = False + for (_opt, _val) in _options: + if _opt == '-i': + _interactive = True + elif _opt == '-c': + exec _val + <BLANKLINE> + if _args: + sys.argv[:] = _args + execfile(sys.argv[0]) + <BLANKLINE> + if _interactive: + import code + code.interact(banner="", local=globals()) + + >>> f.close() + +If we change the Python version to 2.4, we'll use Python 2.4 eggs: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... parts = demo + ... eggs-directory = eggs + ... + ... [demo] + ... recipe = zc.recipe.egg + ... eggs = demo <0.3 + ... find-links = %(server)s + ... index = %(server)s/index + ... python = python2.4 + ... + ... [python2.4] + ... executable = %(python24)s + ... + ... """ % dict(server=link_server, python24=python2_4_executable)) + + >>> print system(buildout), + + >>> ls(sample_buildout, 'eggs') + - demo-0.2-py2.3.egg + - demo-0.2-py2.4.egg + - demoneeded-1.1-py2.3.egg + - demoneeded-1.1-py2.4.egg + - setuptools-0.6-py2.4.egg + - zc.buildout-1.0-py2.4.egg + + >>> if sys.platform == 'win32': + ... f = open(os.path.join(sample_buildout, 'bin', 'demo-script.py')) + ... else: + ... f = open(os.path.join(sample_buildout, 'bin', 'demo')) + >>> f.readline().strip() == '#!' + python2_4_executable + True + >>> print f.read(), # doctest: +NORMALIZE_WHITESPACE + <BLANKLINE> + import sys + sys.path[0:0] = [ + '/private/tmp/tmpOEtRO8sample-buildout/eggs/demo-0.2-py2.4.egg', + '/private/tmp/tmpOEtRO8sample-buildout/eggs/demoneeded-1.1-py2.4.egg' + ] + <BLANKLINE> + import eggrecipedemo + <BLANKLINE> + if __name__ == '__main__': + eggrecipedemo.main() + + >>> f.close() + + >>> if sys.platform == 'win32': + ... f = open(os.path.join(sample_buildout, 'bin', 'py-demo-script.py')) + ... else: + ... f = open(os.path.join(sample_buildout, 'bin', 'py-demo')) + >>> f.readline().strip() == '#!' + python2_4_executable + True + >>> print f.read(), # doctest: +NORMALIZE_WHITESPACE + import sys + <BLANKLINE> + sys.path[0:0] = [ + '/tmp/tmp5zS2Afsample-buildout/eggs/demo-0.2-py2.4.egg', + '/tmp/tmp5zS2Afsample-buildout/eggs/demoneeded-1.1-py2.4.egg' + ] + <BLANKLINE> + _interactive = True + if len(sys.argv) > 1: + import getopt + _options, _args = getopt.getopt(sys.argv[1:], 'ic:') + _interactive = False + for (_opt, _val) in _options: + if _opt == '-i': + _interactive = True + elif _opt == '-c': + exec _val + <BLANKLINE> + if _args: + sys.argv[:] = _args + execfile(sys.argv[0]) + <BLANKLINE> + if _interactive: + import code + code.interact(banner="", local=globals()) + + >>> f.close() diff --git a/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/tests.py b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/tests.py new file mode 100644 index 00000000..fa290cdc --- /dev/null +++ b/zc.setuptools/zc.recipe.egg_/src/zc/recipe/egg/tests.py @@ -0,0 +1,135 @@ +############################################################################## +# +# 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 +import zc.buildout.testing + +import unittest +from zope.testing import doctest, renormalizing + +os_path_sep = os.path.sep +if os_path_sep == '\\': + os_path_sep *= 2 + +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'], + 'develop-eggs', 'zc.recipe.egg.egg-link'), + 'w').write(dirname(__file__, 4)) + zc.buildout.testing.create_sample_eggs(test) + zc.buildout.testing.setUpServer(test, zc.buildout.testing.make_tree(test)) + + +def setUpPython(test): + zc.buildout.testing.buildoutSetUp(test) + + open(os.path.join(test.globs['sample_buildout'], + 'develop-eggs', 'zc.recipe.egg.egg-link'), + 'w').write(dirname(__file__, 4)) + + zc.buildout.testing.multi_python(test) + zc.buildout.testing.setUpServer(test, zc.buildout.testing.make_tree(test)) + +def setUpCustom(test): + zc.buildout.testing.buildoutSetUp(test) + open(os.path.join(test.globs['sample_buildout'], + 'develop-eggs', 'zc.recipe.egg.egg-link'), + 'w').write(dirname(__file__, 4)) + zc.buildout.testing.create_sample_eggs(test) + zc.buildout.testing.add_source_dist(test) + zc.buildout.testing.setUpServer(test, zc.buildout.testing.make_tree(test)) + + +def test_suite(): + return unittest.TestSuite(( + #doctest.DocTestSuite(), + doctest.DocFileSuite( + 'README.txt', + setUp=setUp, tearDown=zc.buildout.testing.buildoutTearDown, + checker=renormalizing.RENormalizing([ + (re.compile('(\S+[/%(sep)s]| )' + '(\\w+-)[^ \t\n%(sep)s/]+.egg' + % dict(sep=os_path_sep) + ), + '\\2-VVV-egg'), + (re.compile('-py\d[.]\d.egg'), '-py2.4.egg'), + (re.compile('zc.buildout(-\S+)?[.]egg(-link)?'), + 'zc.buildout.egg'), + (re.compile('(\n?)- ([a-zA-Z_.-]+)-script.py\n- \\2.exe\n'), + '\\1- \\2\n'), + (re.compile('#![^\n]+python[^\n]*\n'), '#!python\n'), + ]) + ), + doctest.DocFileSuite( + 'api.txt', + setUp=setUp, tearDown=zc.buildout.testing.buildoutTearDown, + checker=renormalizing.RENormalizing([ + (re.compile('_b = \S+sample-buildout.bin'), + '_b = sample-buildout/bin'), + (re.compile('__buildout_signature__ = ' + 'sample-\S+\s+' + 'zc.recipe.egg-\S+\s+' + 'setuptools-\S+\s+' + 'zc.buildout-\S+\s*' + ), + '__buildout_signature__ = sample- zc.recipe.egg-'), + (re.compile('_d = \S+sample-buildout.develop-eggs'), + '_d = sample-buildout/develop-eggs'), + (re.compile('_e = \S+sample-buildout.eggs'), + '_e = sample-buildout/eggs'), + (re.compile('executable = \S+python\S*'), + 'executable = python'), + (re.compile('index = \S+python\S+'), + 'executable = python'), + (re.compile('find-links = http://localhost:\d+/'), + 'find-links = http://localhost:8080/'), + (re.compile('index = http://localhost:\d+/index'), + 'index = http://localhost:8080/index'), + ]) + ), + doctest.DocFileSuite( + 'selecting-python.txt', + setUp=setUpPython, tearDown=zc.buildout.testing.buildoutTearDown, + checker=renormalizing.RENormalizing([ + (re.compile('\S+sample-(\w+)[%(sep)s/](\S+)' + % dict(sep=os_path_sep)), + r'/sample-\1/\2'), + (re.compile('\S+sample-(\w+)'), r'/sample-\1'), + (re.compile('- ([a-zA-Z_0-9.]+)(-\S+)?[.]egg(-link)?'), + '\\1.egg'), + (re.compile(r'\\\\'), '/'), + (re.compile(r'/\\'), '/'), + ]), + ), + doctest.DocFileSuite( + 'custom.txt', + setUp=setUpCustom, tearDown=zc.buildout.testing.buildoutTearDown, + checker=renormalizing.RENormalizing([ + (re.compile("(d ((ext)?demo(needed)?|other)" + "-\d[.]\d-py)\d[.]\d(-[^. \t\n]+)?[.]egg"), + '\\1V.V.egg'), + (re.compile('extdemo.c\n.+\\extdemo.exp\n'), ''), + ]), + ), + + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff --git a/zc.setuptools/zc.recipe.testrunner/README.txt b/zc.setuptools/zc.recipe.testrunner/README.txt new file mode 100644 index 00000000..b2a8266a --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/README.txt @@ -0,0 +1,26 @@ +Test-Runner Recipe +================== + +This recipe generates zope.testing test-runenr scripts for testing a +collection of eggs. The eggs must already be installed (using the +zc.recipe.egg recipe) + +The test-runner recipe has 2 options: + +- The eggs option takes the names of the eggs to be + tested. These are not installed by the recipe. They must be + installed by some other recipe (or using the buildout develop + option). The distributions are in the form os setuptools + requirements. Multiple distributions must be listed on separate + lines. This option is required. + +- The script option gives the name of the script to generate, in the + buildout bin directory. Of the option isn't used, the part name + will be used. + +To do +----- + +- Support specifying testrunner defaults (e.g. verbosity, test file + patterns, etc.) + diff --git a/zc.setuptools/zc.recipe.testrunner/setup.py b/zc.setuptools/zc.recipe.testrunner/setup.py new file mode 100644 index 00000000..93e7985e --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages + +name = "zc.recipe.testrunner" +setup( + name = name, + version = "1.0.0a1", + author = "Jim Fulton", + author_email = "jim@zope.com", + description = "ZC Buildout recipe for creating test runners", + long_description=open('README.txt').read(), + license = "ZPL 2.1", + keywords = "development build testing", + url='http://svn.zope.org/zc.buildout', + + packages = find_packages('src'), + include_package_data = True, + package_dir = {'':'src'}, + namespace_packages = ['zc', 'zc.recipe'], + install_requires = ['zc.buildout', 'zope.testing', 'setuptools', + 'zc.recipe.egg', + ], + test_suite = name+'.tests.test_suite', + entry_points = {'zc.buildout': ['default = %s:TestRunner' % name]}, + dependency_links = ['http://download.zope.org/distribution/'], + ) diff --git a/zc.setuptools/zc.recipe.testrunner/src/zc/__init__.py b/zc.setuptools/zc.recipe.testrunner/src/zc/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/src/zc/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/__init__.py b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/__init__.py new file mode 100644 index 00000000..de40ea7c --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/README.txt b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/README.txt new file mode 100644 index 00000000..19e94271 --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/README.txt @@ -0,0 +1,174 @@ +Test-Runner Recipe +================== + +The test-runner recipe, zc.recipe.testrunner, creates a test runner +for a project. + +The test-runner recipe has 2 options: + +eggs + The eggs option specified a list of eggs to test given as one ore + more setuptools requirement strings. Each string must be given on + a separate line. + +script + The script option gives the name of the script to generate, in the + buildout bin directory. Of the option isn't used, the part name + will be used. + +(Note that, at this time, due to limitations in the Zope test runner, + the distributions cannot be zip files. TODO: Fix the test runner!) + +To illustrate this, we'll create a pair of projects in our sample +buildout: + + >>> mkdir(sample_buildout, 'demo') + >>> mkdir(sample_buildout, 'demo', 'demo') + >>> write(sample_buildout, 'demo', 'demo', '__init__.py', '') + >>> write(sample_buildout, 'demo', 'demo', 'tests.py', + ... ''' + ... import unittest + ... + ... class TestDemo(unittest.TestCase): + ... def test(self): + ... pass + ... + ... def test_suite(): + ... return unittest.makeSuite(TestDemo) + ... ''') + + >>> write(sample_buildout, 'demo', 'setup.py', + ... """ + ... from setuptools import setup + ... + ... setup(name = "demo") + ... """) + + >>> write(sample_buildout, 'demo', 'README.txt', '') + + >>> mkdir(sample_buildout, 'demo2') + >>> mkdir(sample_buildout, 'demo2', 'demo2') + >>> write(sample_buildout, 'demo2', 'demo2', '__init__.py', '') + >>> write(sample_buildout, 'demo2', 'demo2', 'tests.py', + ... ''' + ... import unittest + ... + ... class Demo2Tests(unittest.TestCase): + ... def test2(self): + ... pass + ... + ... def test_suite(): + ... return unittest.makeSuite(Demo2Tests) + ... ''') + + >>> write(sample_buildout, 'demo2', 'setup.py', + ... """ + ... from setuptools import setup + ... + ... setup(name = "demo2", install_requires= ['demoneeded']) + ... """) + + >>> write(sample_buildout, 'demo2', 'README.txt', '') + +Demo 2 depends on demoneeded: + + >>> mkdir(sample_buildout, 'demoneeded') + >>> mkdir(sample_buildout, 'demoneeded', 'demoneeded') + >>> write(sample_buildout, 'demoneeded', 'demoneeded', '__init__.py', '') + >>> write(sample_buildout, 'demoneeded', 'demoneeded', 'tests.py', + ... ''' + ... import unittest + ... + ... class TestNeeded(unittest.TestCase): + ... def test_needed(self): + ... pass + ... + ... def test_suite(): + ... return unittest.makeSuite(TestNeeded) + ... ''') + + >>> write(sample_buildout, 'demoneeded', 'setup.py', + ... """ + ... from setuptools import setup + ... + ... setup(name = "demoneeded") + ... """) + + >>> write(sample_buildout, 'demoneeded', 'README.txt', '') + +We'll update our buildout to install the demo project as a +develop egg and to create the test script: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = demo demoneeded demo2 + ... parts = testdemo + ... offline = true + ... + ... [testdemo] + ... recipe = zc.recipe.testrunner + ... eggs = + ... demo + ... demo2 + ... script = test + ... """) + +Note that we specified both demo and demo2 in the eggs +option and that we put them on separate lines. + +We also specified the offline option to run the buildout in offline mode. + +Now when we run the buildout: + + >>> import os + >>> os.chdir(sample_buildout) + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + +We get a test script installed in our bin directory: + + >>> ls(sample_buildout, 'bin') + - buildout + - py-zc.buildout + - test + +We can run the test script to run our demo test: + + >>> print system(os.path.join(sample_buildout, 'bin', 'test') + ' -vv'), + Running tests at level 1 + Running unit tests: + Running: + test (demo.tests.TestDemo) + test2 (demo2.tests.Demo2Tests) + Ran 2 tests with 0 failures and 0 errors in 0.000 seconds. + +Note that we didn't run the demoneeded tests. Tests are only run for +the eggs listed, not for their dependencies. + +If we leave the script option out of the configuration, then the test +script will get it's name from the part: + + >>> write(sample_buildout, 'buildout.cfg', + ... """ + ... [buildout] + ... develop = demo + ... parts = testdemo + ... offline = true + ... + ... [testdemo] + ... recipe = zc.recipe.testrunner + ... eggs = demo + ... """) + + >>> print system(os.path.join(sample_buildout, 'bin', 'buildout')), + + >>> ls(sample_buildout, 'bin') + - buildout + - py-zc.buildout + - testdemo + +We can run the test script to run our demo test: + + >>> print system(os.path.join(sample_buildout, 'bin', 'testdemo')), + Running unit tests: + Ran 1 tests with 0 failures and 0 errors in 0.000 seconds. diff --git a/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/__init__.py b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/__init__.py new file mode 100644 index 00000000..af3bbde4 --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/__init__.py @@ -0,0 +1,89 @@ +############################################################################## +# +# 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$ +""" + +import os, sys +import pkg_resources +import zc.buildout.easy_install +import zc.recipe.egg + +class TestRunner: + + def __init__(self, buildout, name, options): + self.buildout = buildout + self.name = name + self.options = options + options['script'] = os.path.join(buildout['buildout']['bin-directory'], + options.get('script', self.name), + ) + self.egg = zc.recipe.egg.Egg(buildout, name, options) + + def install(self): + options = self.options + requirements, ws = self.egg.working_set(('zope.testing', )) + + path = [dist.location for dist in ws] + project_names = [ + pkg_resources.Requirement.parse(r).project_name + for r in requirements + ] + + locations = [dist.location for dist in ws + if dist.project_name in project_names] + + result = [] + script = options['script'] + if sys.platform == 'win32': + # generate exe file and give the script a magic name: + open(script+'.exe', 'wb').write( + pkg_resources.resource_string('setuptools', 'cli.exe') + ) + result.append(script+'.exe') + script += '-script.py' + + open(script, 'w').write(tests_template % dict( + PYTHON=options['executable'], + PATH=repr(path)[1:-1].replace(', ', ',\n '), + TESTPATH=repr(locations)[1:-1].replace( + ', ', ",\n '--test-path', "), + )) + try: + os.chmod(script, 0755) + except (AttributeError, os.error): + pass + + result.append(script) + + return result + + +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/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/tests.py b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/tests.py new file mode 100644 index 00000000..429b97af --- /dev/null +++ b/zc.setuptools/zc.recipe.testrunner/src/zc/recipe/testrunner/tests.py @@ -0,0 +1,62 @@ +############################################################################## +# +# 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 zc.recipe.egg + +import unittest +import zope.testing +from zope.testing import doctest, renormalizing + +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.testrunner.egg-link'), + 'w').write(dirname(__file__, 4)) + open(os.path.join(test.globs['sample_buildout'], + 'eggs', 'zc.recipe.egg.egg-link'), + 'w').write(dirname(zc.recipe.egg.__file__, 4)) + + # XXX assumes that zope.testing egg is a directory + open(os.path.join(test.globs['sample_buildout'], + 'eggs', 'zope.testing.egg-link'), + 'w').write(dirname(zope.testing.__file__, 3)) + +def tearDown(test): + 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('(\n?)- ([a-zA-Z_.-]+)-script.py\n- \\2.exe\n'), + '\\1- \\2\n'), + ]) + ), + + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') -- 2.30.9