############################################################################## # # Copyright (c) 2005 Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """Buildout main script $Id$ """ import md5 import os import pprint import re import shutil import sys import ConfigParser import zc.buildout.easy_install import pkg_resources import zc.buildout.easy_install import zc.buildout.egglinker class MissingOption(KeyError): """A required option was missing """ class Options(dict): def __init__(self, buildout, section, data): self.buildout = buildout self.section = section super(Options, self).__init__(data) def __getitem__(self, option): try: return super(Options, self).__getitem__(option) except KeyError: raise MissingOption("Missing option", self.section, option) def copy(self): return Options(self.buildout, self.section, self) class Buildout(dict): def __init__(self): self._buildout_dir = os.path.abspath(os.getcwd()) self._config_file = self.buildout_path('buildout.cfg') super(Buildout, self).__init__(self._open( directory = self._buildout_dir, eggs_directory = 'eggs', bin_directory = 'bin', parts_directory = 'parts', installed = '.installed.cfg', )) options = self['buildout'] links = options.get('find_links', '') self._links = links and links.split() or () for name in ('bin', 'parts', 'eggs'): d = self.buildout_path(options[name+'_directory']) setattr(self, name, d) if not os.path.exists(d): os.mkdir(d) _template_split = re.compile('([$]{\w+:\w+})').split def _open(self, **predefined): # Open configuration files parser = ConfigParser.SafeConfigParser() parser.add_section('buildout') for k, v in predefined.iteritems(): parser.set('buildout', k, v) parser.read(self._config_file) data = dict([ (section, Options(self, section, [(k, v.strip()) for (k, v) in parser.items(section)]) ) for section in parser.sections() ]) converted = {} for section, options in data.iteritems(): for option, value in options.iteritems(): if '$' in value: value = self._dosubs(section, option, value, data, converted, []) options[option] = value converted[(section, option)] = value return data def _dosubs(self, section, option, value, data, converted, seen): key = section, option r = converted.get(key) if r is not None: return r if key in seen: raise ValueError('Circular references', seen, key) seen.append(key) value = '$$'.join([self._dosubs_esc(s, data, converted, seen) for s in value.split('$$') ]) seen.pop() return value def _dosubs_esc(self, value, data, converted, seen): value = self._template_split(value) subs = [] for s in value[1::2]: s = tuple(s[2:-1].split(':')) v = converted.get(s) if v is None: options = data.get(s[0]) if options is None: raise KeyError("Referenced section does not exist", s[0]) v = options.get(s[1]) if v is None: raise KeyError("Referenced option does not exist", *s) if '$' in v: v = 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 install(self): self._develop() new_part_options = self._gather_part_info() installed_part_options = self._read_installed_part_options() old_parts = installed_part_options['buildout']['parts'].split() old_parts.reverse() new_old_parts = [] for part in old_parts: installed_options = installed_part_options[part].copy() installed = installed_options.pop('__buildout_installed__') if installed_options != new_part_options.get(part): self._uninstall(installed) del installed_part_options[part] else: new_old_parts.append(part) new_old_parts.reverse() new_parts = [] try: for part in new_part_options['buildout']['parts'].split(): installed = self._install(part) new_part_options[part]['__buildout_installed__'] = installed new_parts.append(part) installed_part_options[part] = new_part_options[part] new_old_parts = [p for p in new_old_parts if p != part] finally: new_parts.extend(new_old_parts) installed_part_options['buildout']['parts'] = ' '.join(new_parts) self._save_installed_options(installed_part_options) def _develop(self): """Install sources by running setup.py develop on them """ develop = self['buildout'].get('develop') if develop: here = os.getcwd() try: for setup in develop.split(): setup = self.buildout_path(setup) if os.path.isdir(setup): setup = os.path.join(setup, 'setup.py') os.chdir(os.path.dirname(setup)) os.spawnle( os.P_WAIT, sys.executable, sys.executable, setup, '-q', 'develop', '-m', '-x', '-f', ' '.join(self._links), '-d', self.eggs, {'PYTHONPATH': os.path.dirname(pkg_resources.__file__)}, ) finally: os.chdir(os.path.dirname(here)) def _gather_part_info(self): """Get current part info, including part options and recipe info """ parts = self['buildout']['parts'] part_info = {'buildout': {'parts': parts}} recipes_requirements = [] pkg_resources.working_set.add_entry(self.eggs) parts = parts and parts.split() or [] for part in parts: options = self.get(part) if options is None: options = self[part] = {} options = options.copy() recipe, entry = self._recipe(part, options) zc.buildout.easy_install.install( recipe, self.eggs, self._links) recipes_requirements.append(recipe) part_info[part] = options # Load up the recipe distros pkg_resources.require(recipes_requirements) base = self.eggs + os.path.sep for part in parts: options = part_info[part] recipe, entry = self._recipe(part, options) req = pkg_resources.Requirement.parse(recipe) sig = _dists_sig(pkg_resources.working_set.resolve([req]), base) options['__buildout_signature__'] = ' '.join(sig) return part_info def _recipe(self, part, options): recipe = options.get('recipe', part) if ':' in recipe: recipe, entry = recipe.split(':') else: entry = 'default' return recipe, entry def _read_installed_part_options(self): old = self._installed_path() if os.path.isfile(old): parser = ConfigParser.SafeConfigParser() parser.read(old) return dict([(section, dict(parser.items(section))) for section in parser.sections()]) else: return {'buildout': {'parts': ''}} def _installed_path(self): return self.buildout_path(self['buildout']['installed']) def _uninstall(self, installed): for f in installed.split(): f = self.buildout_path(f) if os.path.isdir(f): shutil.rmtree(f) elif os.path.isfile(f): os.remove(f) def _install(self, part): options = self[part] recipe, entry = self._recipe(part, options) recipe_class = pkg_resources.load_entry_point( recipe, 'zc.buildout', entry) installed = recipe_class(self, part, options).install() if installed is None: installed = [] elif isinstance(installed, basestring): installed = [installed] base = self.buildout_path('') installed = [d.startswith(base) and d[len(base):] or d for d in installed] return ' '.join(installed) def _save_installed_options(self, installed_options): parser = ConfigParser.SafeConfigParser() for section in installed_options: parser.add_section(section) for option, value in installed_options[section].iteritems(): parser.set(section, option, value) parser.write(open(self._installed_path(), 'w')) def _dir_hash(dir): hash = md5.new() for (dirpath, dirnames, filenames) in os.walk(dir): filenames[:] = [f for f in filenames if not (f.endswith('pyc') or f.endswith('pyo')) ] hash.update(' '.join(dirnames)) hash.update(' '.join(filenames)) for name in filenames: hash.update(open(os.path.join(dirpath, name)).read()) return hash.digest().encode('base64').strip() def _dists_sig(dists, base): result = [] for dist in dists: location = dist.location if dist.precedence == pkg_resources.DEVELOP_DIST: result.append(dist.project_name + '-' + _dir_hash(location)) else: if location.startswith(base): location = location[len(base):] result.append(location) return result def main(): Buildout().install()