#!/usr/local/bin/python2.1

""" Distutils Extensions needed for the mx Extensions.

    Copyright (c) 1997-2000, Marc-Andre Lemburg; mailto:mal@lemburg.com
    Copyright (c) 2000-2004, eGenix.com Software GmbH; mailto:info@egenix.com
    See the documentation for further information on copyrights,
    or contact the author. All Rights Reserved.
"""
#
# History:
# 2.1.2: added wide Unicode support
# 2.1.1: added support for Python 2.3 way of finding MSVC paths
# 2.1.0: added bdist_zope, support for classifiers
# 2.0.0: reworked the include and lib path logic; factored out the
#        compiler preparation
# 1.9.0: added new include and lib path logic; added bdist_zope command
# Older revisions are not documented.
#
import string, types, glob, os, sys, re
from distutils.errors import \
     DistutilsError, DistutilsExecError, CompileError, CCompilerError
from distutils.core import setup, Extension, Command
from distutils.sysconfig import \
     get_config_h_filename, parse_config_h, customize_compiler, \
     get_python_inc, get_config_vars
from distutils.msvccompiler import MSVCCompiler
from distutils.util import execute, get_platform
from distutils.dir_util import remove_tree, mkpath
from distutils.spawn import spawn, find_executable
from distutils.dist import Distribution
from distutils.command.config import config
from distutils.command.build import build
from distutils.command.build_ext import build_ext
from distutils.command.build_clib import build_clib
from distutils.command.build_py import build_py
from distutils.command.bdist_rpm import bdist_rpm
from distutils.command.bdist_dumb import bdist_dumb
from distutils.command.install import install
from distutils.command.install_data import install_data
import distutils.archive_util

# distutils extensions
if sys.version[:3] >= '2.0':
    try:
        from distutils.command import bdist_ppm
    except ImportError:
        bdist_ppm = None
    try:
        from distutils.command import GenPPD
    except ImportError:
        GenPPD = None
else:
    bdist_ppm = None
    GenPPD = None

### Globals

__version__ = '2.1.1'

_debug = 0

#
# Helpers
#
def find_file(filename, paths, pattern=None):

    """ Look for a file in the directories defined in the list
        paths.

        If pattern is given, the found files are additionally checked
        to include the given RE search pattern. Pattern matching is
        done case-insensitive per default.

        Returns the directory where the file can be found or None in
        case it was not found.

    """
    if _debug: print 'looking for %s ...' % filename
    for dir in paths:
        pathname = os.path.join(dir, filename)
        if os.path.exists(pathname):
            if pattern:
                data = open(pathname).read()
                if not re.search(pattern, data, re.I):
                    data = None
                    if _debug:
                        print ' %s: found, but not matched' % dir
                    continue
                data = None
            if _debug:
                print ' %s: found and matched' % dir
            return dir
        elif _debug:
            print ' %s: not found' % dir
    if _debug: print 'not found'
    return None

def add_dir(dir, pathlist, index=-1):

    if dir not in pathlist and \
       os.path.isdir(dir):
        if index < 0:
            index = index + len(pathlist) + 1
        pathlist.insert(index, dir)

def py_version(unicode_aware=1, include_patchlevel=0):

    """ Return the Python version as short string.

        If unicode_aware is true (default), the function also tests
        whether a UCS2 or UCS4 built is running and modifies the
        version accordingly.

        If include_patchlevel is true (default is false), the patch
        level is also included in the version string.

    """
    if include_patchlevel:
        version = sys.version[:5]
    else:
        version = sys.version[:3]
    if unicode_aware and version > '2.0':
        try:
            unichr(100000)
        except ValueError:
            pass
        else:
            # UCS4 build
            version = version + 'ucs4'
    return version

def check_zope_product_version(version, version_txt):

    """ Check whether the version string version matches the
        version data in the Zope product version.txt file
        version_txt.

    """
    data = open(version_txt, 'r').read().strip()
    return data[-len(version):] == version

def mx_customize_compiler(compiler):

    # Allow environment variables to override settings in the
    # Python Makefile on Unix; this overrides
    # distutils.sysconfig.customize_compiler()
    if compiler.compiler_type == "unix":
        # Note: BASECFLAGS is new in Python 2.3; thanks to
        # David M. Cooke for pointing this out to us.
        cc, opt, ccshared, basecflags, ldshared, so = \
             get_config_vars('CC', 'OPT', 'BASECFLAGS', 'CCSHARED',
                             'LDSHARED', 'SO')
        cc = os.environ.get('CC', cc or '')
        opt = os.environ.get('OPT', opt or '')
        basecflags = os.environ.get('BASECFLAGS', basecflags or '')
        ccshared = os.environ.get('CCSHARED', ccshared or '')
        ldshared = os.environ.get('LDSHARED', ldshared or '')
        so = os.environ.get('SO', so or '')
        compiler.set_executables(
            preprocessor='%s -E' % cc,
            compiler='%s %s %s' % (cc, opt, basecflags),
            compiler_so='%s %s %s %s' % (cc, opt, ccshared, basecflags),
            linker_so=ldshared,
            linker_exe=cc)
        compiler.shared_lib_extension = so

    else:
        customize_compiler(compiler)

compression_programs = {
    'gzip': ('.gz', '-f9'),
    'bzip2': ('.bz2', 'f9'),
    'compress': ('.Z', '-f'),
    }

def mx_make_tarball(base_name, base_dir, compression='gzip', verbose=0,
                    dry_run=0, tar_options='-chf'):

    # Much like archive_util.make_tarball, except that this version
    # dereferences symbolic links.
    tar_archive = base_name + '.tar'

    # Create the directory for the archive
    mkpath(os.path.dirname(tar_archive), dry_run=dry_run)

    # Create the archive
    cmd = ['tar', tar_options, tar_archive, base_dir]
    spawn(cmd, verbose=verbose, dry_run=dry_run)

    # Compress if that's needed
    if compression:
        try:
            ext, options = compression_programs[compression]
        except KeyError:
            raise ValueError, 'unknown compression program: %s' % compress
        cmd = [compression, options, tar_archive]
        spawn(cmd, verbose=verbose, dry_run=dry_run)
        tar_archive = tar_archive + ext
        
    return tar_archive

# Register our version of make_tarball()
distutils.archive_util.ARCHIVE_FORMATS.update({
    'gztar': (mx_make_tarball, [('compression', 'gzip')], 'gzipped tar-file'),
    'bztar': (mx_make_tarball, [('compression', 'bzip2')], 'bzip2ed tar-file'),
    'ztar':  (mx_make_tarball, [('compression', 'compress')], 'compressed tar file'),
    'tar':   (mx_make_tarball, [('compression', None)], 'tar file'),
    })

def build_path(dirs):

    """ Builds a path list from a list of directories/paths.

        The dirs list may contain shell variable references and user
        dir references. These will get expanded
        automatically. Non-existing shell variables are replaced with
        an empty string. Path entries will get expanded to single
        directory entries.  Empty string entries are removed from the
        list.

    """
    try:
        expandvars = os.path.expandvars
    except AttributeError:
        expandvars = None
    try:
        expanduser = os.path.expanduser
    except AttributeError:
        expanduser = None
    path = []
    for i in range(len(dirs)):
        dir = dirs[i]
        if expanduser is not None:
            dir = expanduser(dir)
        if expandvars is not None:
            dir = expandvars(dir)
            if '$' in dir:
                dir = string.join(re.split(r'\$\w+|\{[^}]*\}', dir), '')
        dir = string.strip(dir)
        if os.pathsep in dir:
            path.extend(string.split(dir, os.pathsep))
        elif dir:
            path.append(dir)
        # empty entries are omitted
    return path

def verify_path(path):

    """ Verify the directories in path for existence and their
        directory nature.

        Also removes duplicates from the list.

    """
    d = {}
    l = []
    for dir in path:
        if os.path.exists(dir) and \
           os.path.isdir(dir):
            if not d.has_key(dir):
                d[dir] = 1
                l.append(dir)
    path[:] = l

def get_msvc_paths():

    """ Return a tuple (libpath, inclpath) defining the search
        paths for library files and include files that the MS VC++
        compiler uses per default.

        Both entries are lists of directories.

        Only available on Windows platforms with installed compiler.

    """
    try:
        # distutils versions prior to the one that came with Python 2.3
        from distutils.msvccompiler import get_devstudio_versions, get_msvc_paths
        msvc_versions = get_devstudio_versions()
        if msvc_versions:
            msvc_version = msvc_versions[0] # choose most recent one
            inclpath = get_msvc_paths('include', msvc_version)
            libpath = get_msvc_paths('lib', msvc_version)
        else:
            libpath = []
            inclpath = []
            
    except ImportError:
        # distutils Python 2.3 and later
        from distutils.msvccompiler import MSVCCompiler
        try:
            msvccompiler = MSVCCompiler()
            inclpath = msvccompiler.get_msvc_paths('include')
            libpath = msvccompiler.get_msvc_paths('library')
        except Exception, why:
            print '*** Problem:', why
            libpath = []
            inclpath = []
        msvccompiler = None

    return libpath, inclpath

#
# Search paths
#
        
# Standard system directories which are automatically scanned by the
# compiler and linker for include files and libraries. LIB and INCLUDE
# are environment variables used on Windows platforms, other platforms
# may have different names.
STDLIBPATH = build_path(['/usr/lib', '/opt/lib', '$LIB'])
STDINCLPATH = build_path(['/usr/include', '/opt/include', '$INCLUDE'])

# Add additional dirs from Windows registry if available
if sys.platform[:3] == 'win':
    libpath, inclpath = get_msvc_paths()
    STDLIBPATH.extend(libpath)
    STDINCLPATH.extend(inclpath)

verify_path(STDLIBPATH)
verify_path(STDINCLPATH)

# Default paths for additional library and include file search (in
# addition to the standard system directories above); these are always
# added to the compile and link commands by mxSetup per default.
LIBPATH = build_path(['/usr/local/lib',
                      os.path.join(sys.prefix, 'lib')])
INCLPATH = build_path(['/usr/local/include',
                       os.path.join(sys.prefix, 'include')])

verify_path(LIBPATH)
verify_path(INCLPATH)

if _debug:
    print 'mxSetup found these paths:'
    print ' lib path:', LIBPATH
    print ' include path:', INCLPATH

# Additional paths to scan in order to find third party libs and
# headers; these are used by mx_autoconf.find_*_file() APIs.
FINDLIBPATH = build_path([])
FINDINCLPATH = build_path([])

verify_path(FINDLIBPATH)
verify_path(FINDINCLPATH)

if 0:
    # Work-around for quirk on Solaris which happens to be a common
    # problem when compiling things with GCC: there's a non-GCC stdarg.h
    # header file in /usr/include which gets picked up by GCC instead of
    # its own compiler specific one, so we remove /usr/include from
    # INCLPATH in that situation.
    if sys.platform == 'sunos5' and \
       string.find(sys.version, 'GCC'):
        if os.path.exists('/usr/include/stdarg.h'):
            INCLPATH.remove('/usr/include')

if _debug:
    print 'mxSetup will additionally scan these paths during autoconf:'
    print ' lib path:', FINDLIBPATH
    print ' include path:', FINDINCLPATH

#
# Mixin helpers
#

class CompilerSupportMixin:

    """ Compiler support mixin which makes sure that the .compiler
        attribute is properly setup.
    
    """
    # Internal flag
    prepared_compiler = 0

    def prepare_compiler(self):

        if self.prepared_compiler:
            return

        # Setup .compiler instance
        if self.compiler is None:
            self._check_compiler()
        compiler = self.compiler
        
        # Work around a bug in distutils <= 1.0.3
        if compiler.exe_extension is None:
            compiler.exe_extension = ''

        # Make sure we have a typical setup for directory
        # searches
        for dir in LIBPATH:
            add_dir(dir, compiler.library_dirs)
        for dir in INCLPATH:
            add_dir(dir, compiler.include_dirs)

        # Customize the compiler according to system settings
        mx_customize_compiler(self.compiler)
        
        self.prepared_compiler = 1

#
# mx Auto-Configuration command
#

class mx_autoconf(CompilerSupportMixin,
                  config):

    """ Auto-configuration class which adds some extra configuration
        settings to the packages.

    """
    # Command description
    description = "auto-configuration build step (for internal use only)"

    # Command line options
    user_options = config.user_options + [
        ('enable-debugging', None,
         'compile with debugging support'),
        ]

    # User option defaults
    enable_debugging = 0

    # C APIs to check: (C API name, list of header files to include)
    api_checks = (('strftime', ['time.h']),
                  ('strptime', ['time.h']),
                  ('timegm', ['time.h']),
                  )

    def initialize_options(self):

        config.initialize_options(self)
        self.noisy = 0
        self.dump_source = 0

    def finalize_options(self):

        config.finalize_options(self)

    def prepare_compiler(self):

        if self.prepared_compiler:
            return

        # Setup .compiler instance
        if not self.compiler:
            self._check_compiler()
            mx_customize_compiler(self.compiler)

        compiler = self.compiler
        
        # Work around a bug in distutils <= 1.0.3
        if compiler.exe_extension is None:
            compiler.exe_extension = ''

        # Make sure we have a typical setup for directory
        # searches; also see mx_Extension.prepared_compiler()
        for dir in LIBPATH:
            add_dir(dir, compiler.library_dirs)
        for dir in INCLPATH:
            add_dir(dir, compiler.include_dirs)

        self.prepared_compiler = 1

    def run(self):

        # Setup .compiler instance
        self.prepare_compiler()        

        # Add some static #defines which should not hurt
        self.compiler.define_macro('_GNU_SOURCE', '1')

        # Parse [py]config.h
        config_h = get_config_h_filename()
        try:
            configfile = open(config_h)
        except IOError,why:
            self.warn('could not open %s file' % config_h)
            configuration = {}
        else:
            configuration = parse_config_h(configfile)
            configfile.close()

        # Tweak configuration a little for some platforms
        if sys.platform[:5] == 'win32':
            configuration['HAVE_STRFTIME'] = 1

        # Build lists of #defines and #undefs
        define = []
        undef = []

        # Check APIs
        for apiname, includefiles in self.api_checks:
            macro_name = 'HAVE_' + string.upper(apiname)
            if not configuration.has_key(macro_name):
                if self.check_function(apiname, includefiles):
                    define.append((macro_name, '1'))
                else:
                    undef.append(macro_name)

        # Compiler tests
        if not configuration.has_key('BAD_STATIC_FORWARD'):
            if self.check_bad_staticforward():
                define.append(('BAD_STATIC_FORWARD', '1'))

        # Enable debugging support
        if self.enable_debugging:
            define.append(('MAL_DEBUG', None))

        self.announce('macros to define: %s' % define)
        self.announce('macros to undefine: %s' % undef)

        # Reinitialize build_ext command with extra defines
        build_ext = self.distribution.reinitialize_command('build_ext')
        build_ext.ensure_finalized()
        # We set these here, since distutils 1.0.2 introduced a
        # new incompatible way to process .define and .undef
        if build_ext.define:
            #print repr(build_ext.define)
            if type(build_ext.define) is types.StringType:
                # distutils < 1.0.2 needs this:
                l = string.split(build_ext.define, ',')
                build_ext.define = map(lambda symbol: (symbol, '1'), l)
            build_ext.define = build_ext.define + define
        else:
            build_ext.define = define
        if build_ext.undef:
            #print repr(build_ext.undef)
            if type(build_ext.undef) is types.StringType:
                # distutils < 1.0.2 needs this:
                build_ext.undef = string.split(build_ext.undef, ',')
            build_ext.undef = build_ext.undef + undef
        else:
            build_ext.undef = undef
        self.announce('updated build_ext with autoconf setup')

    def check_compiler(self, sourcecode, headers=None, include_dirs=None,
                       libraries=None, library_dirs=None):

        """ Check whether sourcecode compiles and links with the current
            compiler and link environment.

            For documentation of the other arguments see the base
            class' .try_link().
        
        """
        self.prepare_compiler()
        return self.try_link(sourcecode, headers, include_dirs,
                             libraries, library_dirs)

    def check_bad_staticforward(self):

        """ Check whether the compiler does not supports forward declaring
            static arrays.

            For documentation of the other arguments see the base
            class' .try_link().
        
        """
        body = """
        typedef struct _mxstruct {int a; int b;} mxstruct;
        staticforward mxstruct mxarray[];
        statichere mxstruct mxarray[] = {{0,2},{3,4},};
        int main(void) {return mxarray[0].a;}
        """
        self.prepare_compiler()
        return not self.try_compile(body,
                                    headers=('Python.h',),
                                    include_dirs=[get_python_inc()])

    def check_function(self, function, headers=None, include_dirs=None,
                       libraries=None, library_dirs=None,
                       prototype=0, call=0):

        """ Check whether function is available in the given
            compile and link environment.

            If prototype is true, a function prototype is included in
            the test. If call is true, a function call is generated
            (rather than just a reference of the function symbol).

            For documentation of the other arguments see the base
            class' .try_link().
        
        """
        body = []
        if prototype:
            body.append("int %s (void);" % function)
        body.append("int main (void) {")
        if call:
            body.append("  %s();" % function)
        else:
            body.append("  %s;" % function)
        body.append("return 0; }")
        body = string.join(body, "\n") + "\n"
        return self.check_compiler(body, headers, include_dirs,
                                   libraries, library_dirs)

    def check_library(self, library, library_dirs=None,
                      headers=None, include_dirs=None, other_libraries=[]):

        """ Check whether we can link against the given library.

            For documentation of the other arguments see the base
            class' .try_link().
        
        """
        body = "int main (void) { return 0; }"
        return self.check_compiler(body, headers, include_dirs,
                                   [library]+other_libraries, library_dirs)

    def find_include_file(self, filename, paths, pattern=None):

        """ Find an include file of the given name.

            The search path is determined by the paths parameter, the
            compiler's .include_dirs attribute and the STDINCLPATH and
            FINDINCLPATH globals. The search is done in this order.

        """
        self.prepare_compiler()
        paths = (paths
                 + self.compiler.include_dirs
                 + STDINCLPATH
                 + FINDINCLPATH)
        verify_path(paths)
        if _debug: print 'INCLPATH', paths
        return find_file(filename, paths, pattern)

    def find_library_file(self, libname, paths, pattern=None,
                          lib_types=('shared', 'static')):

        """ Find a library of the given name.

            The search path is determined by the paths parameter, the
            compiler's .library_dirs attribute and the STDLIBPATH and
            FINDLIBPATH globals. The search is done in this order.

            Shared libraries are prefered over static ones if both
            types are given in lib_types.

        """
        self.prepare_compiler()
        paths = (paths
                 + self.compiler.library_dirs
                 + STDLIBPATH
                 + FINDLIBPATH)
        verify_path(paths)
        if _debug: print 'LIBPATH', paths

        # Try to first find a shared library to use and revert
        # to a static one, if no shared lib can be found
        for lib_type in lib_types:
            filename = self.compiler.library_filename(libname,
                                                      lib_type=lib_type)
            dir = find_file(filename, paths, pattern)
            if dir is not None:
                return dir
            
        return None

#
# mx MSVC Compiler extension
#
# We want some extra options for the MSVCCompiler, so we add them
# here. This is an awful hack, but there seems to be no other way to
# subclass a standard distutils C compiler class...

if sys.version[:3] < '2.4':
    # VC6
    MSVC_COMPILER_FLAGS = ['/O2', '/Gf', '/GB', '/GD', '/Ob2']
else:
    # VC7
    MSVC_COMPILER_FLAGS = ['/O2', '/GF', '/GB', '/Ob2']

# remember old __init__
old_MSVCCompiler__init__ = MSVCCompiler.__init__

def mx_msvccompiler__init__(self, *args, **kws):

    apply(old_MSVCCompiler__init__, (self,) + args, kws)
    self.compile_options.extend(MSVC_COMPILER_FLAGS)

# "Install" new __init__
MSVCCompiler.__init__ = mx_msvccompiler__init__

#
# mx Distribution class
#

class mx_Distribution(Distribution):

    """ Distribution class which knows about our distutils extensions.
        
    """
    # List of UnixLibrary instances
    unixlibs = None

    # Add classifiers dummy options if needed
    if 'classifiers' not in Distribution.display_options:
        display_options = Distribution.display_options + [
        ('classifiers', None,
         "print the list of classifiers (not yet supported)"),
        ]
        display_option_names = Distribution.display_option_names + [
            'classifiers'
            ]

    def has_unixlibs(self):
        return self.unixlibs and len(self.unixlibs) > 0

#
# mx Extension class
#

class mx_Extension(Extension):

    """ Extension class which allows specifying whether the extension
        is required to build or optional.
        
    """
    # Is this Extension required to build or can we issue a Warning in
    # case it fails to build and continue with the remaining build
    # process ?
    required = 1

    # List of optional libaries (libname, list of header files to
    # check) to include in the link step; the availability of these is
    # tested prior to compiling the extension. If found, the symbol
    # HAVE_<upper(libname)>_LIB is defined and the library included in
    # the list of libraries to link against.
    optional_libraries = ()

    # List of needed include files in form of tuples (filename,
    # [dir1, dir2,...], pattern); see mx_autoconf.find_file()
    # for details
    needed_includes = ()

    # List of needed include files in form of tuples (libname,
    # [dir1, dir2,...], pattern); see mx_autoconf.find_library_file()
    # for details
    needed_libraries = ()

    # Library types to check (for needed libraries)
    lib_types = ('shared', 'static')

    # Data files needed by this extension (these are only
    # installed if building the extension succeeded)
    data_files = ()

    # Python packages needed by this extension (these are only
    # installed if building the extension succeeded)
    packages = ()

    # Building success marker
    successfully_built = 0

    def __init__(self, *args, **kws):
        for attr in ('required',
                     'lib_types',
                     'optional_libraries',
                     'needed_includes',
                     'needed_libraries',
                     'data_files',
                     'packages'):
            if kws.has_key(attr):
                setattr(self, attr, kws[attr])
                del kws[attr]
            else:
                value = getattr(self, attr)
                if type(value) == type(()):
                    setattr(self, attr, list(value))
        apply(Extension.__init__, (self,) + args, kws)

#
# mx Build command
#

class mx_build(build):

    """ build command which knows about our distutils extensions.
        
    """
    def has_unixlibs(self):
        return self.distribution.has_unixlibs()

    if len(build.sub_commands) > 4:
        raise DistutilsError, 'incompatible distutils version !'
    sub_commands = [('build_clib',    build.has_c_libraries),
                    ('build_unixlib', has_unixlibs),
                    ('mx_autoconf',   build.has_ext_modules),
                    ('build_ext',     build.has_ext_modules),
                    ('build_py',      build.has_pure_modules),
                    ('build_scripts', build.has_scripts),
                   ]

#
# mx Build C Lib command
#

class mx_build_clib(build_clib):

    """ build_clib command which builds the libs using
        separate temp dirs
        
    """
    def build_library(self, lib_name, build_info):

        # Build each extension in its own subdir of build_temp (to
        # avoid accidental sharing of object files between extensions
        # having the same name, e.g. mxODBC.o).
        build_temp_base = self.build_temp
        self.build_temp = os.path.join(build_temp_base, lib_name)

        try:

            #
            # This is mostly identical to the original build_clib command.
            #
            sources = build_info.get('sources')
            if sources is None or \
               type(sources) not in (types.ListType, types.TupleType):
                raise DistutilsSetupError, \
                      ("in 'libraries' option (library '%s'), " +
                       "'sources' must be present and must be " +
                       "a list of source filenames") % lib_name
            sources = list(sources)

            self.announce("building '%s' library" % lib_name)

            # First, compile the source code to object files in the
            # library directory.  (This should probably change to
            # putting object files in a temporary build directory.)
            macros = build_info.get('macros')
            include_dirs = build_info.get('include_dirs')
            objects = self.compiler.compile(sources,
                                            output_dir=self.build_temp,
                                            macros=macros,
                                            include_dirs=include_dirs,
                                            debug=self.debug)

            # Now "link" the object files together into a static library.
            # (On Unix at least, this isn't really linking -- it just
            # builds an archive.  Whatever.)
            self.compiler.create_static_lib(objects, lib_name,
                                            output_dir=self.build_clib,
                                            debug=self.debug)
            
            
        finally:
            # Cleanup local changes to the configuration
            self.build_temp = build_temp_base
        
    def build_libraries(self, libraries):

        for (lib_name, build_info) in libraries:
            self.build_library(lib_name, build_info)

#
# mx Build Extensions command
#

class mx_build_ext(CompilerSupportMixin,
                   build_ext):

    """ build_ext command which runs mx_autoconf command before
        trying to build anything.
        
    """
    user_options = build_ext.user_options + [
        ('disable-build=', None,
         'disable building an optional extensions (comma separated list of '
         'dotted package names); default is to try building all'),
        ('enable-build=', None,
         'if given, only these optional extensions are built (comma separated '
         'list of dotted package names)'),
        ]

    # mx_autoconf command object (finalized and run)
    autoconf = None

    # Default values for command line options
    disable_build = None
    enable_build = None
    
    def finalize_options(self):

        build_ext.finalize_options(self)
        if self.disable_build is None:
            self.disable_build = ()
        elif type(self.disable_build) is types.StringType:
            self.disable_build = map(string.strip,
                                     string.split(self.disable_build, ','))
        if type(self.enable_build) is types.StringType:
            self.enable_build = map(string.strip,
                                    string.split(self.enable_build, ','))

    def run(self):

        # Add unixlibs install-dirs to library_dirs, so that linking
        # against them becomes easy
        if self.distribution.has_unixlibs():
            build_unixlib = self.get_finalized_command('build_unixlib')
            paths, libs = build_unixlib.get_unixlib_lib_options()
            # Libraries have to be linked against by defining them
            # in the mx_Extension() setup, otherwise, all extensions
            # get linked against all Unix libs that were built...
            #self.libraries[:0] = libs
            self.library_dirs[:0] = paths
            
        # Assure that mx_autoconf has been run and store a reference
        # in .autoconf
        self.run_command('mx_autoconf')
        self.autoconf = self.get_finalized_command('mx_autoconf')

        # Now, continue with the standard build process
        build_ext.run(self)

    def build_extensions(self):

        # Make sure the compiler is setup correctly
        self.prepare_compiler()
        
        # Make sure that any autoconf actions use the same compiler
        # settings as we do (the .compiler is set up in build_ext.run()
        # just before calling .build_extensions())
        self.autoconf.compiler = self.compiler

        # Build the extensions
        self.check_extensions_list(self.extensions)
        for ext in self.extensions:
            self.build_extension(ext)

        # Cleanup .extensions list (remove entries which did not build correctly)
        l = []
        for ext in self.extensions:
            if not isinstance(ext, mx_Extension):
                l.append(ext)
            else:
                if ext.successfully_built:
                    l.append(ext)
        self.extensions = l
        if _debug: print 'extensions:', repr(self.extensions)
        self.announce('')
         
    def build_extension(self, ext):

        required = not hasattr(ext, 'required') or ext.required
        self.announce('')
        self.announce('building extension "%s" %s' %
                      (ext.name,
                       required * '(required)' or '(optional)'))

        # Optional extension building can be adjusted via command line options
        if not required:
            if self.enable_build is not None and \
               ext.name not in self.enable_build:
                self.announce('skipped -- build not enabled by command line option')
                return
            elif ext.name in self.disable_build:
                self.announce('skipped -- build disabled by command line option')
                return

        # Search for include files
        if hasattr(ext, 'needed_includes') and \
           ext.needed_includes:
            self.announce('looking for header files needed by extension '
                          '"%s"' % ext.name)
            for filename, dirs, pattern in ext.needed_includes:
                dir = self.autoconf.find_include_file(filename,
                                                      dirs,
                                                      pattern)
                if dir is not None:
                    self.announce('found needed include file "%s" '
                                  'in directory %s' % (filename, dir))
                    if dir not in ext.include_dirs and \
                       dir not in STDINCLPATH and \
                       dir not in INCLPATH:
                        ext.include_dirs.append(dir)
                else:
                    if required:
                        raise CompileError, \
                              'could not find needed header file "%s"' % \
                              filename
                    else:
                        self.announce(
                            '*** WARNING: Building of extension '
                            '"%s" failed: needed include file "%s" '
                            'not found' %
                            (ext.name, filename))
                        return
                    
        # Search for libraries
        if hasattr(ext, 'needed_libraries') and \
           ext.needed_libraries:
            self.announce('looking for libraries needed by extension '
                          '"%s"' % ext.name)
            for libname, dirs, pattern in ext.needed_libraries:
                dir = self.autoconf.find_library_file(libname,
                                                      dirs,
                                                      pattern,
                                                      ext.lib_types)
                if dir is not None:
                    self.announce('found needed library "%s" '
                                  'in directory %s' % (libname, dir))
                    if dir not in ext.library_dirs and \
                       dir not in STDLIBPATH and \
                       dir not in LIBPATH:
                        ext.library_dirs.append(dir)
                    if libname not in ext.libraries:
                        ext.libraries.append(libname)
                else:
                    if required:
                        raise CompileError, \
                              'could not find needed library "%s"' % \
                              libname
                    else:
                        self.announce(
                            '*** WARNING: Building of extension '
                            '"%s" failed: needed library "%s" '
                            'not found' %
                            (ext.name, libname))
                        return
                    
        # Build each extension in its own subdir of build_temp (to
        # avoid accidental sharing of object files between extensions
        # having the same name, e.g. mxODBC.o).
        build_temp_base = self.build_temp
        extpath = string.join(string.split(ext.name, '.'), os.sep)
        self.build_temp = os.path.join(build_temp_base, extpath)

        # Check for availability of optional libs which can be used
        # by the extension; note: this step includes building small
        # object files to test for the availability of the libraries
        if hasattr(ext, 'optional_libraries') and \
           ext.optional_libraries:
            self.announce("checking for optional libraries")
            for libname, headerfiles in ext.optional_libraries:
                if self.autoconf.check_library(libname, headers=headerfiles):
                    symbol = 'HAVE_%s_LIB' % string.upper(libname)
                    self.announce("found optional library '%s'"
                                  " -- defining %s" % (libname, symbol))
                    ext.libraries.append(libname)
                    ext.define_macros.append((symbol, '1'))
                else:
                    self.announce("could not find optional library '%s'"
                                  " -- omitting it" % libname)

        if _debug:
            print 'Include dirs:', repr(ext.include_dirs +
                                        self.compiler.include_dirs)
            print 'Libary dirs:', repr(ext.library_dirs +
                                       self.compiler.library_dirs)
            print 'Libaries:', repr(ext.libraries)
            print 'Macros:', repr(ext.define_macros)

        # Build the extension
        successfully_built = 0
        try:
            
            # Skip extensions which cannot be built if the required
            # option is given and set to false.
            required = not hasattr(ext, 'required') or ext.required
            if required:
                build_ext.build_extension(self, ext)
                successfully_built = 1
            else:
                try:
                    build_ext.build_extension(self, ext)
                except (CCompilerError, DistutilsError), why:
                    self.announce(
                        '*** WARNING: Building of extension "%s" '
                        'failed: %s' %
                        (ext.name, sys.exc_info()[1]))
                else:
                    successfully_built = 1

        finally:
            # Cleanup local changes to the configuration
            self.build_temp = build_temp_base

        # Processing for successfully built extensions
        if successfully_built:
            
            # Add Python packages needed by this extension
            if hasattr(ext, 'packages'):
                self.distribution.packages.extend(ext.packages)

            # Add data files needed by this extension
            if hasattr(ext, 'data_files'):
                self.distribution.data_files.extend(ext.data_files)

            # Mark as having been built successfully
            ext.successfully_built = 1

#
# mx Build Python command
#

class mx_build_py(build_py):

    """ build_py command which also allows removing Python source code
        after the byte-code compile process.
        
    """
    user_options = build_py.user_options + [
        ('without-source', None, "only include Python byte-code"),
        ]

    boolean_options = build_py.boolean_options + ['without-source']

    # Omit source files ?
    without_source = 0
    
    def run(self):

        if self.without_source:
            # --without-source implies byte-code --compile
            if not self.compile and \
               not self.optimize:
                self.compile = 1

        # Build the Python code
        build_py.run(self)

        # Optionally remove source code
        if self.without_source:
            self.announce('removing Python source files (--without-source)')
            verbose = self.verbose
            dry_run = self.dry_run
            for file in build_py.get_outputs(self, include_bytecode=0):
                # Only process .py files
                if file[-3:] != '.py':
                    continue
                # Remove source code
                execute(os.remove, (file,), "removing %s" % file,
                        verbose=verbose, dry_run=dry_run)
                # Remove .pyc files (if not requested)
                if not self.compile:
                    filename = file + "c"
                    if os.path.isfile(filename):
                        execute(os.remove, (filename,),
                                "removing %s" % filename,
                                verbose=verbose, dry_run=dry_run)
                # Remove .pyo files (if not requested)
                if self.optimize == 0:
                    filename = file + "o"
                    if os.path.isfile(filename):
                        execute(os.remove, (filename,),
                                "removing %s" % filename,
                                verbose=verbose, dry_run=dry_run)

    def get_outputs(self, include_bytecode=1):

        if not self.without_source or \
           not include_bytecode:
            return build_py.get_outputs(self, include_bytecode)

        # Remove source code files from outputs
        files = []
        for file in build_py.get_outputs(self, include_bytecode=1):
            if file[-3:] == '.py' or \
               (not self.compile and file[-4:] == '.pyc') or \
               (not self.optimize and file[-4:] == '.pyo'):
                continue
            files.append(file)
        return files
        
#
# mx Build Unix Libs command
#
class UnixLibrary:

    """ Container for library configuration data.
    """
    # Name of the library
    libname = ''

    # Source tree where the library lives
    sourcetree = ''

    # List of library files and where to install them in the
    # build tree
    libfiles = None

    # Name of the configure script
    configure = 'configure'

    # Configure options
    configure_options = None

    # Make options
    make_options = None
    
    def __init__(self, libname, sourcetree, libfiles,
                 configure=None, configure_options=None,
                 make_options=None):

        self.libname = libname
        self.sourcetree = sourcetree
        # Check for 2-tuples...
        for libfile, targetdir in libfiles:
            pass
        self.libfiles = libfiles

        # Optional settings
        if configure:
            self.configure = configure
        if configure_options:
            self.configure_options = configure_options
        else:
            self.configure_options = []
        if make_options:
            self.make_options = make_options
        else:
            self.make_options = []
            
    def get(self, option, alternative=None):

        return getattr(self, option, alternative)

class mx_build_unixlib(Command):

    """ This command compiles external libs using the standard Unix
        procedure for this:
        
        ./configure
        make

    """
    description = "build Unix libraries used by Python extensions"

    # make program to use
    make = None
    
    user_options = [
        ('build-lib=', 'b',
         "directory to store built Unix libraries in"),
        ('build-temp=', 't',
         "directory to build Unix libraries to"),
        ('make=', None,
         "make program to use"),
        ('makefile=', None,
         "makefile to use"),
        ('force', 'f',
         "forcibly reconfigure"),
        ]
    
    boolean_options = ['force']

    def initialize_options(self):

        self.build_lib = None
        self.build_temp = None
        self.make = None
        self.makefile = None
        self.force = 0

    def finalize_options(self):

        self.set_undefined_options('build',
                                   ('verbose', 'verbose'),
                                   ('build_lib', 'build_lib'),
                                   ('build_temp', 'build_temp')
                                   )
        if self.make is None:
            self.make = 'make'
        if self.makefile is None:
            self.makefile = 'Makefile'

        # For debugging we are always in very verbose mode...
        self.verbose = 2

    def run_script(self, script, options=[]):

        if options:
            l = []
            for k, v in options:
                if v is not None:
                    l.append('%s=%s' % (k, v))
                else:
                    l.append(k)
            script = script + ' ' + string.join(l, ' ')
        if self.verbose > 1:
            self.announce('executing script %s' % repr(script))
        if self.dry_run:
            return 0
        try:
            rc = os.system(script)
        except DistutilsExecError, msg:
            raise CompileError, msg
        return rc
    
    def run_configure(self, options=[], dir=None, configure='configure'):

        """ Run the configure script using options is given.

            Options must be a list of tuples (optionname,
            optionvalue).  If an option should not have a value,
            passing None as optionvalue will have the effect of using
            the option without value.

            dir can be given to have the configure script execute in
            that directory instead of the current one.

        """
        cmd = './%s' % configure
        if dir:
            cmd = 'cd %s; ' % dir + cmd
        if self.verbose:
            self.announce('running %s in %s' % (configure, dir or '.'))
        rc = self.run_script(cmd, options)
        if rc != 0:
            raise CompileError, 'configure script failed'

    def run_make(self, targets=[], dir=None, make='make', options=[]):

        """ Run the make command for the given targets.

            Targets must be a list of valid Makefile targets.

            dir can be given to have the make program execute in that
            directory instead of the current one.

        """
        cmd = '%s' % make
        if targets:
            cmd = cmd + ' ' + string.join(targets, ' ')
        if dir:
            cmd = 'cd %s; ' % dir + cmd
        if self.verbose:
            self.announce('running %s in %s' % (make, dir or '.'))
        rc = self.run_script(cmd, options)
        if rc != 0:
            raise CompileError, 'make failed'

    def build_unixlib(self, unixlib):

        # Build each lib in its own subdir of build_temp (to
        # avoid accidental sharing of object files)
        build_temp_base = self.build_temp
        libpath = unixlib.libname
        self.build_temp = os.path.join(build_temp_base, libpath)

        try:

            # Get options
            configure = unixlib.configure
            configure_options = unixlib.configure_options
            make_options = unixlib.make_options
            sourcetree = unixlib.sourcetree
            buildtree = os.path.join(self.build_temp, sourcetree)
            libfiles = unixlib.libfiles
            if not libfiles:
                raise DistutilsError, \
                      'no libfiles defined for unixlib "%s"' % \
                      unixlib.name
            if self.verbose:
                self.announce('building C lib %s in %s' % (unixlib.libname,
                                                           buildtree))
            # Prepare build
            if self.verbose:
                self.announce('preparing build of %s' % unixlib.libname)
            self.mkpath(buildtree)
            self.copy_tree(sourcetree, buildtree)

            # Run configure to build the Makefile
            if not os.path.exists(os.path.join(buildtree, self.makefile)) or \
               self.force:
                self.run_configure(configure_options,
                                   dir=buildtree,
                                   configure=configure)
            elif self.verbose:
                self.announce("skipping configure: "
                              "%s is already configured" %
                              unixlib.libname)

            # Run make
            self.run_make(dir=buildtree,
                          make=self.make,
                          options=make_options)

            # Copy libs to destinations
            for sourcefile, destination in libfiles:
                if not destination:
                    continue
                sourcefile = os.path.join(self.build_temp, sourcefile)
                destination = os.path.join(self.build_lib, destination)
                if not os.path.exists(sourcefile):
                    raise CompileError, \
                          'library "%s" failed to build' % sourcefile
                self.mkpath(destination)
                self.copy_file(sourcefile, destination)

        finally:
            # Cleanup local changes to the configuration
            self.build_temp = build_temp_base

    def build_unixlibs(self, unixlibs):

        for unixlib in unixlibs:
            self.build_unixlib(unixlib)

    def get_unixlib_lib_options(self):

        libs = []
        paths = []
        for unixlib in self.distribution.unixlibs:
            for sourcefile, destination in unixlib.libfiles:
                if not destination:
                    # direct linking
                    sourcefile = os.path.join(self.build_temp,
                                              sourcefile)
                    libs.append(sourcefile)
                else:
                    # linking via link path and lib name
                    sourcefile = os.path.basename(sourcefile)
                    libname = os.path.splitext(sourcefile)[0]
                    if libname[:3] == 'lib':
                        libname = libname[3:]
                    libs.append(libname)
                    destination = os.path.join(self.build_lib,
                                               destination)
                    paths.append(destination)
        #print paths, libs
        return paths, libs

    def run(self):

        if not self.distribution.unixlibs:
            return
        self.build_unixlibs(self.distribution.unixlibs)

#
# mx Install command
#

class mx_install(install):

    """ We want install_data to default to install_purelib
        if it is not given.

    """
    def finalize_options(self):

        fix_install_data = (self.install_data is None)
        install.finalize_options(self)
        if fix_install_data:
            self.install_data = self.install_purelib

    # Hack needed for bdist_wininst
    def ensure_finalized(self):

        install.ensure_finalized(self)
        if self.install_data[-5:] == '\\DATA':
            self.install_data = self.install_data[:-5] + '\\PURELIB'

#
# mx Install Data command
#

class mx_install_data(install_data):

    """ Rework the install_data command to something more useful.
    """
    def finalize_options(self):

        if self.install_dir is None:
            installobj = self.distribution.get_command_obj('install')
            self.install_dir = installobj.install_data
        if _debug: print 'Installing data files to %s' % self.install_dir
        install_data.finalize_options(self)

    def run (self):

        if not self.dry_run:
            self.mkpath(self.install_dir)
        data_files = self.get_inputs()
        for entry in data_files:
            if type(entry) == types.StringType:
                if 1 or os.name != 'posix':
                    # Unix- to platform-convention conversion
                    entry = string.join(string.split(entry, '/'), os.sep)
                filenames = glob.glob(entry)
                for filename in filenames:
                    dst = os.path.join(self.install_dir, filename)
                    dstdir = os.path.split(dst)[0]
                    if not self.dry_run:
                        self.mkpath(dstdir)
                        outfile = self.copy_file(filename, dst)[0]
                    else:
                        outfile = dst
                    self.outfiles.append(outfile)
            else:
                raise ValueError, 'tuples in data_files not supported'

#
# mx Uninstall command
#
# Credits are due to Thomas Heller for the idea and the initial code
# base for this command (he posted a different version to
# distutils-sig@python.org) in 02/2001.
#

class mx_uninstall(Command):

    description = "uninstall the package files and directories"

    user_options = []

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass
        
    def run(self):

        # Execute build
        self.announce('determining installation files')
        self.announce('(re)building package')
        savevalue = self.distribution.dry_run
        self.distribution.dry_run = 0
        self.run_command('build')

        # Execute install in dry-run mode
        self.announce('dry-run package install')
        self.distribution.dry_run = 1
        self.run_command('install')
        self.distribution.dry_run = savevalue
        build = self.get_finalized_command('build')
        install = self.get_finalized_command('install')

        # Remove all installed files
        self.announce("removing files")
        dirs = {}
        filenames = install.get_outputs()
        for filename in filenames:
            if not os.path.isabs(filename):
                raise DistutilsError,\
                      'filename %s from .get_output() not absolute' % \
                      filename

            if os.path.isfile(filename):
                self.announce("removing %s" % filename)
                if not self.dry_run:
                    try:
                        os.remove(filename)
                    except OSError, details:
                        self.warn("Could not remove file: %s" % details)
                    dir = os.path.split(filename)[0]
                    if not dirs.has_key(dir):
                        dirs[dir] = 1
                    if os.path.splitext(filename)[1] == '.py':
                        # Remove byte-code files as well
                        try:
                            os.remove(filename + 'c')
                        except OSError:
                            pass
                        try:
                            os.remove(filename + 'o')
                        except OSError:
                            pass

            elif os.path.isdir(filename):
                # This functionality is currently not being used by distutils
                if not dirs.has_key(dir):
                    dirs[filename] = 1

            elif not os.path.splitext(filename)[1] in ('.pyo', '.pyc'):
                self.announce("skipping removal of %s (not found)" %
                              filename)

        # Remove the installation directories
        self.announce("removing directories")
        dirs = dirs.keys()
        dirs.sort(); dirs.reverse() # sort descending
        for dir in dirs:
            self.announce("removing directory %s" % dir)
            if not self.dry_run:
                try:
                    os.rmdir(dir)
                except OSError, details:
                    self.warn("could not remove directory: %s" % details)
#
# mx RPM distribution command
#

class mx_bdist_rpm(bdist_rpm):

    """ bdist_rpm command which allows passing in distutils
        options.

    """
    user_options = bdist_rpm.user_options + [
        ('distutils-build-options=', None,
         'extra distutils build options to use before the "build" command'),
        ('distutils-install-options=', None,
         'extra distutils install options to use after the "install" command'),
        ]

    # Defaults
    distutils_build_options = None
    distutils_install_options = None

    def finalize_options(self):

        bdist_rpm.finalize_options(self)
        if self.distutils_build_options is None:
            self.distutils_build_options = ''
        if self.distutils_install_options is None:
            self.distutils_install_options = ''

    def _make_spec_file(self):

        # Generate .spec file
        l = bdist_rpm._make_spec_file(self)

        # Insert into build command
        i = l.index('%build')
        buildcmd = l[i + 1]
        inspos = string.find(l[i + 1], ' build')
        if inspos >= 0:
            l[i + 1] = '%s %s %s' % (buildcmd[:inspos],
                                     self.distutils_build_options,
                                     buildcmd[inspos:])
        else:
            raise DistutilsError, \
                  'could not insert distutils command in RPM build command'
        
        # Insert into install command
        i = l.index('%install')
        installcmd = l[i + 1]
        inspos = string.find(l[i + 1], ' install')
        if inspos >= 0:
            l[i + 1] = '%s %s %s %s' % (installcmd[:inspos],
                                        self.distutils_build_options,
                                        installcmd[inspos:],
                                        self.distutils_install_options)
        else:
            raise DistutilsError, \
                  'could not insert distutils command in RPM install command'

        return l
    
#
# mx in-place binary distribution command
#

class mx_bdist_inplace(bdist_dumb):

    """ Build binary Zope product distribution.
    """

    # Path prefix to use in the distribution (all files will be placed
    # under this prefix)
    dist_prefix = ''

    user_options = bdist_dumb.user_options + [
        ('dist-prefix=', None,
         'path prefix the binary distribution with'),
        ]

    def finalize_options(self):

        # Default to ZIP as format on all platforms
        if self.format is None:
            self.format = 'zip'
        # Default to <platform>-py<version> on all platforms
        if self.plat_name is None:
            self.plat_name = '%s-py%s' % (get_platform(), py_version())
        bdist_dumb.finalize_options(self)

    # Hack to reuse bdist_dumb for our purposes; .run() calls
    # reinitialize_command() with 'install' as command.
    def reinitialize_command(self, command, reinit_subcommands=0):

        cmdobj = bdist_dumb.reinitialize_command(self, command,
                                                 reinit_subcommands)
        if command == 'install':
            cmdobj.install_lib = self.dist_prefix
            cmdobj.install_data = self.dist_prefix
        return cmdobj

#
# mx Zope binary distribution command
#

class mx_bdist_zope(mx_bdist_inplace):

    """ Build binary Zope product distribution.
    """

    # Path prefix to use in the distribution (all files will be placed
    # under this prefix)
    dist_prefix = 'lib/python'

###

def run_setup(configurations):

    """ Run distutils setup.

        The parameters passed to setup() are extracted from the list
        of modules, classes or instances given in configurations.

        Names with leading underscore are removed from the parameters.
        Parameters which are not strings, lists or tuples are removed
        as well.  Configurations which occur later in the
        configurations list override settings of configurations
        earlier in the list.

    """
    # Build parameter dictionary
    kws = {}
    for configuration in configurations:
        kws.update(vars(configuration))
    for name, value in kws.items():
        if name[:1] == '_' or \
           type(value) not in (types.StringType,
                               types.ListType,
                               types.TupleType):
            del kws[name]
        #if type(value) is types.FunctionType:
        #    kws[name] = value()

    # Add setup extensions
    kws['distclass'] = mx_Distribution
    extensions = {'build': mx_build,
                  'build_unixlib': mx_build_unixlib,
                  'mx_autoconf': mx_autoconf,
                  'build_ext': mx_build_ext,
                  'build_clib': mx_build_clib,
                  'build_py': mx_build_py,
                  'install': mx_install,
                  'install_data': mx_install_data,
                  'uninstall': mx_uninstall,
                  'bdist_rpm': mx_bdist_rpm,
                  'bdist_zope': mx_bdist_zope,
                  'bdist_inplace': mx_bdist_inplace,
                  }
    if bdist_ppm is not None:
        extensions['bdist_ppm'] = bdist_ppm.bdist_ppm
    if GenPPD is not None:
        extensions['GenPPD'] = GenPPD.GenPPD
    kws['cmdclass'] = extensions

    # Invoke distutils setup
    apply(setup, (), kws)

