buildout.py 35.6 KB
Newer Older
jim's avatar
jim committed
1
#############################################################################
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#
# 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$
"""

jim's avatar
jim committed
19
import logging
20 21 22 23 24 25
import md5
import os
import pprint
import re
import shutil
import sys
26
import tempfile
27 28 29
import ConfigParser

import pkg_resources
jim's avatar
jim committed
30
import zc.buildout
31 32
import zc.buildout.easy_install

jim's avatar
jim committed
33 34
pkg_resources_loc = pkg_resources.working_set.find(
    pkg_resources.Requirement.parse('setuptools')).location
35 36


jim's avatar
jim committed
37
class MissingOption(zc.buildout.UserError, KeyError):
38 39 40
    """A required option was missing
    """

jim's avatar
jim committed
41
class MissingSection(zc.buildout.UserError, KeyError):
42 43 44
    """A required section is missinh
    """

45 46 47 48 49 50 51 52 53 54 55
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:
56 57
            raise MissingOption("Missing option: %s:%s"
                                % (self.section, option))
58

59 60 61 62 63 64
    # 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)

65 66 67 68 69
    def copy(self):
        return Options(self.buildout, self.section, self)

class Buildout(dict):

jim's avatar
jim committed
70
    def __init__(self, config_file, cloptions, windows_restart=False):
71 72
        config_file = os.path.abspath(config_file)
        self._config_file = config_file
jim's avatar
jim committed
73
        self.__windows_restart = windows_restart
74 75 76
        if not os.path.exists(config_file):
            print 'Warning: creating', config_file
            open(config_file, 'w').write('[buildout]\nparts = \n')
77 78 79 80

        super(Buildout, self).__init__()

        # default options
jim's avatar
jim committed
81 82 83
        data = dict(buildout={
            'directory': os.path.dirname(config_file),
            'eggs-directory': 'eggs',
84
            'develop-eggs-directory': 'develop-eggs',
jim's avatar
jim committed
85 86 87 88 89
            'bin-directory': 'bin',
            'parts-directory': 'parts',
            'installed': '.installed.cfg',
            'python': 'buildout',
            'executable': sys.executable,
jim's avatar
jim committed
90
            'log-level': 'INFO',
jim's avatar
jim committed
91 92
            'log-format': '%(name)s: %(message)s',
            })
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110

        # 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
jim's avatar
jim committed
111
                # The egg dire
112

113
        # do substitutions
114 115 116 117 118 119 120 121 122
        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

123 124 125 126 127 128 129 130 131 132 133
        # 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']
134
        for name in ('bin', 'parts', 'eggs', 'develop-eggs'):
135 136
            d = self._buildout_path(options[name+'-directory'])
            options[name+'-directory'] = d
137

jim's avatar
jim committed
138 139 140
        options['installed'] = os.path.join(options['directory'],
                                            options['installed'])

jim's avatar
jim committed
141 142
        self._setup_logging()

143 144 145 146 147 148
    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:
jim's avatar
jim committed
149 150 151 152 153 154 155
            raise zc.buildout.UserError(
                "Circular reference in substitutions.\n"
                "We're evaluating %s\nand are referencing: %s.\n"
                % (", ".join([":".join(k) for k in seen]),
                   ":".join(key)
                   )
                )
156 157 158 159 160 161 162
        seen.append(key)
        value = '$$'.join([self._dosubs_esc(s, data, converted, seen)
                           for s in value.split('$$')
                           ])
        seen.pop()
        return value

163 164 165
    _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
166 167 168
    def _dosubs_esc(self, value, data, converted, seen):
        value = self._template_split(value)
        subs = []
169 170 171 172
        for ref in value[1::2]:
            s = tuple(ref[2:-1].split(':'))
            if not self._valid(ref):
                if len(s) < 2:
jim's avatar
jim committed
173 174 175
                    raise zc.buildout.UserError("The substitution, %s,\n"
                                                "doesn't contain a colon."
                                                % ref)
176
                if len(s) > 2:
jim's avatar
jim committed
177 178 179
                    raise zc.buildout.UserError("The substitution, %s,\n"
                                                "has too many colons."
                                                % ref)
180
                if not self._simple(s[0]):
jim's avatar
jim committed
181 182 183 184
                    raise zc.buildout.UserError(
                        "The section name in substitution, %s,\n"
                        "has invalid characters."
                        % ref)
185
                if not self._simple(s[1]):
jim's avatar
jim committed
186 187 188 189
                    raise zc.buildout.UserError(
                        "The option name in substitution, %s,\n"
                        "has invalid characters."
                        % ref)
190
                
191 192 193 194
            v = converted.get(s)
            if v is None:
                options = data.get(s[0])
                if options is None:
195 196
                    raise MissingSection(
                        "Referenced section does not exist", s[0])
197 198
                v = options.get(s[1])
                if v is None:
199 200
                    raise MissingOption("Referenced option does not exist:",
                                        *s)
201
                if '$' in v:
202
                    v = self._dosubs(s[0], s[1], v, data, converted, seen)
203 204 205 206 207 208 209
                    options[s[1]] = v
                converted[s] = v
            subs.append(v)
        subs.append('')

        return ''.join([''.join(v) for v in zip(value[::2], subs)])

210
    def _buildout_path(self, *names):
211 212
        return os.path.join(self._buildout_dir, *names)

jim's avatar
jim committed
213
    def bootstrap(self, args):
214
        self._setup_directories()
jim's avatar
jim committed
215 216 217 218 219 220 221

        # 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:
222
                dest = os.path.join(self['buildout']['develop-eggs-directory'],
jim's avatar
jim committed
223 224 225 226 227 228 229 230
                                    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):
jim's avatar
jim committed
231 232 233 234
                    if os.path.isdir(dist.location):
                        shutil.copytree(dist.location, dest)
                    else:
                        shutil.copy2(dist.location, dest)
jim's avatar
jim committed
235 236 237 238 239 240 241 242

        # 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'])

243
    def install(self, install_parts):
jim's avatar
jim committed
244
        self._load_extensions()
245
        self._setup_directories()
jim's avatar
jim committed
246

247 248 249 250
        # Add develop-eggs directory to path so that it gets searched
        # for eggs:
        sys.path.insert(0, self['buildout']['develop-eggs-directory'])

251 252 253
        # Check for updates. This could cause the process to be rstarted
        self._maybe_upgrade()

254
        # load installed data
255
        installed_part_options = self._read_installed_part_options()
256

257 258 259 260 261 262 263 264 265
        # Remove old develop eggs
        self._uninstall(
            installed_part_options['buildout'].get(
                'installed_develop_eggs', '')
            )

        # Build develop eggs
        installed_develop_eggs = self._develop()

266 267 268 269 270 271 272 273 274 275 276 277 278
        # 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:
jim's avatar
jim committed
279 280 281 282
                self._error(
                    'Invalid install parts: %s.\n'
                    'Install parts must be listed in the configuration.',
                    ' '.join(extra))
283 284 285 286 287 288 289 290 291 292 293
            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)

294
        try:
295 296 297 298 299
            # 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()
jim's avatar
jim committed
300
                    installed_files = old_options.pop('__buildout_installed__')
301 302
                    new_options = self.get(part)
                    if old_options == new_options:
jim's avatar
jim committed
303 304 305 306 307 308 309 310 311 312 313 314
                        # The options are the same, but are all of the
                        # installed files still there?  If not, we should
                        # reinstall.
                        if not installed_files:
                            continue
                        for f in installed_files.split('\n'):
                            if not os.path.exists(self._buildout_path(f)):
                                break
                        else:
                            continue

                    # output debugging info
315 316 317 318 319 320 321 322 323 324 325 326 327
                    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)
jim's avatar
jim committed
328

329 330 331 332
                elif not uninstall_missing:
                    continue

                # ununstall part
jim's avatar
jim committed
333
                self._logger.info('Uninstalling %s', part)
334 335 336 337 338 339
                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:
jim's avatar
jim committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
                signature = self[part].pop('__buildout_signature__')
                saved_options = self[part].copy()
                if part in installed_parts:
                    self._logger.info('Updating %s', part)
                    old_options = installed_part_options[part]
                    old_installed_files = old_options['__buildout_installed__']
                    try:
                        update = recipes[part].update
                    except AttributeError:
                        update = recipes[part].install
                        self._logger.warning(
                            "The recipe for %s doesn't define an update "
                            "method. Using it's install method",
                            part)
                        
                    try:
                        installed_files = update()
                    except:
                        installed_parts.remove(part)
                        self._uninstall(old_installed_files)
                        raise
                    
                    if installed_files is None:
                        installed_files = old_installed_files.split('\n')

                else:
                    self._logger.info('Installing %s', part)
                    installed_files = recipes[part].install()
                    if installed_files is None:
                        self._logger.warning(
                            "The %s install returned None.  A path or "
                            "iterable os paths should be returned.",
                            part)
                        installed_files = ()
                    
375 376
                if isinstance(installed_files, str):
                    installed_files = [installed_files]
jim's avatar
jim committed
377 378 379 380 381 382

                installed_part_options[part] = saved_options
                saved_options['__buildout_installed__'
                              ] = '\n'.join(installed_files)
                saved_options['__buildout_signature__'] = signature

383 384
                if part not in installed_parts:
                    installed_parts.append(part)
jim's avatar
jim committed
385

386
        finally:
387 388 389 390 391
            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] 
            )
392 393 394
            installed_part_options['buildout']['installed_develop_eggs'
                                               ] = installed_develop_eggs
            
395 396
            self._save_installed_options(installed_part_options)

397 398 399 400 401 402 403 404 405
    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)

406 407 408 409
    def _develop(self):
        """Install sources by running setup.py develop on them
        """
        develop = self['buildout'].get('develop')
410 411 412 413 414 415 416 417 418
        if not develop:
            return ''

        dest = self['buildout']['develop-eggs-directory']
        old_files = os.listdir(dest)

        env = dict(os.environ, PYTHONPATH=pkg_resources_loc)
        here = os.getcwd()
        try:
419 420
            try:
                for setup in develop.split():
421
                    setup = self._buildout_path(setup)
422 423
                    if os.path.isdir(setup):
                        setup = os.path.join(setup, 'setup.py')
jim's avatar
jim committed
424 425 426 427

                    self._logger.info("Develop: %s", setup)


428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
                    fd, tsetup = tempfile.mkstemp()
                    try:
                        os.write(fd, runsetup_template % dict(
                            setuptools=pkg_resources_loc,
                            setupdir=os.path.dirname(setup),
                            setup=setup,
                            __file__ = setup,
                            ))

                        args = [
                            zc.buildout.easy_install._safe_arg(tsetup),
                            '-q', 'develop', '-mxN',
                            '-f', zc.buildout.easy_install._safe_arg(
                                ' '.join(self._links)
                                ),
                            '-d', zc.buildout.easy_install._safe_arg(dest),
                            ]

jim's avatar
jim committed
446 447 448 449 450 451 452 453
                        if self._log_level <= logging.DEBUG:
                            if self._log_level == logging.DEBUG:
                                del args[1]
                            else:
                                args[1] == '-v'
                            self._logger.debug("in: %s\n%r",
                                               os.path.dirname(setup), args)

454 455 456 457 458 459 460 461
                        assert os.spawnl(
                            os.P_WAIT, sys.executable, sys.executable,
                            *args) == 0

                    finally:
                        os.close(fd)
                        os.remove(tsetup)

462 463 464 465 466 467 468 469 470
            except:
                # if we had an error, we need to roll back changes, by
                # removing any files we created.
                self._sanity_check_develop_eggs_files(dest, old_files)
                self._uninstall('\n'.join(
                    [os.path.join(dest, f)
                     for f in os.listdir(dest)
                     if f not in old_files
                     ]))
471 472
                raise
                     
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
            else:
                self._sanity_check_develop_eggs_files(dest, old_files)
                return '\n'.join([os.path.join(dest, f)
                                  for f in os.listdir(dest)
                                  if f not in old_files
                                  ])

        finally:
            os.chdir(here)


    def _sanity_check_develop_eggs_files(self, dest, old_files):
        for f in os.listdir(dest):
            if f in old_files:
                continue
            if not (os.path.isfile(os.path.join(dest, f))
                    and f.endswith('.egg-link')):
                self._logger.warning(
                    "Unexpected entry, %s, in develop-eggs directory", f)
492

493 494
    def _load_recipes(self, parts):
        recipes = {}
495 496 497
        if not parts:
            return recipes
        
498
        recipes_requirements = []
499 500
        pkg_resources.working_set.add_entry(
            self['buildout']['develop-eggs-directory'])
501
        pkg_resources.working_set.add_entry(self['buildout']['eggs-directory'])
502

503
        # Gather requirements
504 505 506
        for part in parts:
            options = self.get(part)
            if options is None:
507 508
                raise MissingSection("No section was specified for part", part)

509
            recipe, entry = self._recipe(part, options)
jim's avatar
jim committed
510 511
            if recipe not in recipes_requirements:
                recipes_requirements.append(recipe)
512

513 514 515
        # Install the recipe distros
        offline = self['buildout'].get('offline', 'false')
        if offline not in ('true', 'false'):
alga's avatar
alga committed
516
            self._error('Invalid value for offline option: %s', offline)
517
            
jim's avatar
jim committed
518 519 520 521 522 523 524 525 526 527 528 529 530 531
        if offline == 'false':
            dest = self['buildout']['eggs-directory']
        else:
            dest = None

        ws = zc.buildout.easy_install.install(
            recipes_requirements, dest,
            links=self._links,
            index=self['buildout'].get('index'),
            path=[self['buildout']['develop-eggs-directory'],
                  self['buildout']['eggs-directory'],
                  ],
            working_set=pkg_resources.working_set,
            )
532

533
        # instantiate the recipes
534
        for part in parts:
535 536 537 538 539 540 541 542 543 544 545 546 547 548
            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
        for part in parts:
            options = self.get(part)
            if options is None:
                options = self[part] = {}
549 550
            recipe, entry = self._recipe(part, options)
            req = pkg_resources.Requirement.parse(recipe)
551
            sig = _dists_sig(pkg_resources.working_set.resolve([req]))
552 553 554
            options['__buildout_signature__'] = ' '.join(sig)

    def _recipe(self, part, options):
555
        recipe = options['recipe']
556 557 558 559 560 561 562 563 564 565
        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):
566
            parser = ConfigParser.SafeConfigParser(_spacey_defaults)
567
            parser.optionxform = lambda s: s
568
            parser.read(old)
569
            return dict([
570 571 572 573 574 575
                (section,
                 Options(self, section,
                         [item for item in parser.items(section)
                          if item[0] not in _spacey_defaults]
                         )
                 )
576
                for section in parser.sections()])
577
        else:
578
            return {'buildout': Options(self, 'buildout', {'parts': ''})}
579 580

    def _installed_path(self):        
581
        return self._buildout_path(self['buildout']['installed'])
582 583

    def _uninstall(self, installed):
jim's avatar
jim committed
584 585 586
        for f in installed.split('\n'):
            if not f:
                continue
587
            f = self._buildout_path(f)
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
            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]
603
        base = self._buildout_path('')
604 605 606 607
        installed = [d.startswith(base) and d[len(base):] or d
                     for d in installed]
        return ' '.join(installed)

608

609
    def _save_installed_options(self, installed_options):
610 611 612 613 614 615
        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()
jim's avatar
jim committed
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639

    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)
640 641 642

        level -= verbosity
        root_logger.setLevel(level)
jim's avatar
jim committed
643
        self._log_level = level
644 645 646 647 648 649 650 651

        if level <= logging.DEBUG:
            sections = list(self)
            sections.sort()
            print 'Configuration data:'
            for section in sections:
                _save_options(section, self[section], sys.stdout)
            print    
652

653 654
    def _maybe_upgrade(self):
        # See if buildout or setuptools need to be upgraded.
jim's avatar
jim committed
655 656 657 658 659
        # If they do, do the upgrade and restart the buildout process.

        if self['buildout'].get('offline') == 'true':
            return # skip upgrade in offline mode:
        
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
        ws = zc.buildout.easy_install.install(
            [
            (spec + ' ' + self['buildout'].get(spec+'-version', '')).strip()
            for spec in ('zc.buildout', 'setuptools')
            ],
            self['buildout']['eggs-directory'],
            links = self['buildout'].get('find-links', '').split(),
            index = self['buildout'].get('index'),
            path = [self['buildout']['develop-eggs-directory']],
            )

        upgraded = []
        for project in 'zc.buildout', 'setuptools':
            req = pkg_resources.Requirement.parse(project)
            if ws.find(req) != pkg_resources.working_set.find(req):
                upgraded.append(ws.find(req))

        if not upgraded:
            return
jim's avatar
jim committed
679

680 681 682 683 684 685 686 687
        if (os.path.abspath(sys.argv[0])
            != os.path.join(os.path.abspath(self['buildout']['bin-directory']),
                            'buildout')
            ):
            self._logger.warn("Not upgrading because not running a local "
                              "buildout command")
            return

jim's avatar
jim committed
688 689 690 691 692 693 694
        if sys.platform == 'win32' and not self.__windows_restart:
            args = map(zc.buildout.easy_install._safe_arg, sys.argv)
            args.insert(1, '-W')
            if not __debug__:
                args.insert(0, '-O')
            args.insert(0, sys.executable)
            os.execv(sys.executable, args)            
695
        
jim's avatar
jim committed
696 697
        self._logger.info("Upgraded:\n  %s;\nrestarting.",
                          ",\n  ".join([("%s version %s"
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
                                       % (dist.project_name, dist.version)
                                       )
                                      for dist in upgraded
                                      ]
                                     ),
                          )
                
        # the new dist is different, so we've upgraded.
        # Update the scripts and return True
        zc.buildout.easy_install.scripts(
            ['zc.buildout'], ws, sys.executable,
            self['buildout']['bin-directory'],
            )

        # Restart
        args = map(zc.buildout.easy_install._safe_arg, sys.argv)
        if not __debug__:
            args.insert(0, '-O')
        args.insert(0, sys.executable)
        sys.exit(os.spawnv(os.P_WAIT, sys.executable, args))

719 720 721 722 723 724 725
    def _load_extensions(self):
        specs = self['buildout'].get('extensions', '').split()
        if specs:
            if self['buildout'].get('offline') == 'true':
                dest = None
            else:
                dest = self['buildout']['eggs-directory']
jim's avatar
jim committed
726 727 728 729
                if not os.path.exists(dest):
                    self._logger.info('Creating directory %s', dest)
                    os.mkdir(dest)
                    
730 731 732 733 734 735 736
            zc.buildout.easy_install.install(
                specs, dest,
                path=[self['buildout']['develop-eggs-directory']],
                working_set=pkg_resources.working_set,
                )
            for ep in pkg_resources.iter_entry_points('zc.buildout.extension'):
                ep.load()(self)
737

jim's avatar
jim committed
738
    def setup(self, args):
739 740 741 742 743 744 745 746 747 748
        setup = args.pop(0)
        if os.path.isdir(setup):
            setup = os.path.join(setup, 'setup.py')

        self._logger.info("Running setup script %s", setup)
        setup = os.path.abspath(setup)

        fd, tsetup = tempfile.mkstemp()
        try:
            os.write(fd, runsetup_template % dict(
749
                setuptools=pkg_resources_loc,
750 751
                setupdir=os.path.dirname(setup),
                setup=setup,
752
                __file__ = setup,
753 754 755 756 757 758 759
                ))
            os.spawnl(os.P_WAIT, sys.executable, sys.executable, tsetup,
                      *[zc.buildout.easy_install._safe_arg(a)
                        for a in args])
        finally:
            os.close(fd)
            os.remove(tsetup)
jim's avatar
jim committed
760 761

    runsetup = setup # backward compat
762 763 764 765 766 767
        
runsetup_template = """
import sys
sys.path.insert(0, %(setuptools)r)
import os, setuptools

768 769
__file__ = %(__file__)r

770 771 772 773
os.chdir(%(setupdir)r)
sys.argv[0] = %(setup)r
execfile(%(setup)r)
"""
774

775

776 777 778
_spacey_nl = re.compile('[ \t\r\f\v]*\n[ \t\r\f\v\n]*'
                        '|'
                        '^[ \t\r\f\v]+'
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804
                        '|'
                        '[ \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',
    )

805 806 807 808 809
def _save_options(section, options, f):
    print >>f, '[%s]' % section
    items = options.items()
    items.sort()
    for option, value in items:
810 811
        value = value.replace('%', '%%')
        value = _spacey_nl.sub(_quote_spacey_nl, value)
812 813 814 815
        if value.startswith('\n\t'):
            value = '%(__buildout_space_n__)s' + value[2:]
        if value.endswith('\n\t'):
            value = value[:-2] + '%(__buildout_space_n__)s'
816 817
        print >>f, option, '=', value
            
818
    
819 820 821 822 823 824 825 826 827

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:
jim's avatar
jim committed
828
        raise zc.buildout.UserError("Recursive file include", seen, filename)
829 830 831 832 833 834 835

    base = os.path.dirname(filename)
    seen.append(filename)

    result = {}

    parser = ConfigParser.SafeConfigParser()
836
    parser.optionxform = lambda s: s
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859
    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
    

860 861 862 863 864 865 866 867 868 869 870 871
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()
    
872
def _dists_sig(dists):
873 874 875 876 877 878
    result = []
    for dist in dists:
        location = dist.location
        if dist.precedence == pkg_resources.DEVELOP_DIST:
            result.append(dist.project_name + '-' + _dir_hash(location))
        else:
879
            result.append(os.path.basename(location))
880 881
    return result

882 883 884 885 886 887 888 889 890
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):
891
    sys.stderr.write('Error: ' + ' '.join(message) +'\n')
892 893
    sys.exit(1)

jim's avatar
jim committed
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
_usage = """\
Usage: buildout [options] [assignments] [command [command arguments]]

Options:

  -h, --help

     Print this message and exit.

  -v

     Increase the level of verbosity.  This option can be used multiple times.

  -q

alga's avatar
alga committed
909
     Decrease the level of verbosity.  This option can be used multiple times.
jim's avatar
jim committed
910 911 912 913

  -c config_file

     Specify the path to the buildout configuration file to be used.
alga's avatar
alga committed
914 915
     This defaults to the file named "buildout.cfg" in the current
     working directory.
jim's avatar
jim committed
916 917

Assignments are of the form: section:option=value and are used to
alga's avatar
alga committed
918
provide configuration options that override those given in the
jim's avatar
jim committed
919 920 921 922 923
configuration file.  For example, to run the buildout in offline mode,
use buildout:offline=true.

Options and assignments can be interspersed.

alga's avatar
alga committed
924
Commands:
jim's avatar
jim committed
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942

  install [parts]

    Install parts.  If no command arguments are given, then the parts
    definition from the configuration file is used.  Otherwise, the
    arguments specify the parts to be installed.

  bootstrap

    Create a new buildout in the current working directory, copying
    the buildout and setuptools eggs and, creating a basic directory
    structure and a buildout-local buildout script.

"""
def _help():
    print _usage
    sys.exit(0)

943 944 945 946
def main(args=None):
    if args is None:
        args = sys.argv[1:]

jim's avatar
jim committed
947 948
    config_file = 'buildout.cfg'
    verbosity = 0
949
    options = []
jim's avatar
jim committed
950
    windows_restart = False
jim's avatar
jim committed
951 952 953 954
    while args:
        if args[0][0] == '-':
            op = orig_op = args.pop(0)
            op = op[1:]
jim's avatar
jim committed
955
            while op and op[0] in 'vqhW':
jim's avatar
jim committed
956 957
                if op[0] == 'v':
                    verbosity += 10
jim's avatar
jim committed
958
                elif op[0] == 'q':
jim's avatar
jim committed
959
                    verbosity -= 10
jim's avatar
jim committed
960 961
                elif op[0] == 'W':
                    windows_restart = True
jim's avatar
jim committed
962 963
                else:
                    _help()
jim's avatar
jim committed
964
                op = op[1:]
965
                
jim's avatar
jim committed
966 967 968 969 970 971 972 973 974 975
            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:
jim's avatar
jim committed
976 977
                if orig_op == '--help':
                    _help()
jim's avatar
jim committed
978 979 980 981 982 983 984 985 986 987 988 989 990 991
                _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)))
992 993 994

    if args:
        command = args.pop(0)
jim's avatar
jim committed
995
        if command not in ('install', 'bootstrap', 'runsetup', 'setup'):
996 997 998 999
            _error('invalid command:', command)
    else:
        command = 'install'

jim's avatar
jim committed
1000
    try:
1001
        try:
jim's avatar
jim committed
1002
            buildout = Buildout(config_file, options, windows_restart)
1003
            getattr(buildout, command)(args)
jim's avatar
jim committed
1004
        except zc.buildout.UserError, v:
1005 1006
            _error(str(v))
            
jim's avatar
jim committed
1007
    finally:
1008
            logging.shutdown()
1009 1010 1011 1012 1013 1014

if sys.version_info[:2] < (2, 4):
    def reversed(iterable):
        result = list(iterable);
        result.reverse()
        return result