#!/usr/bin/env python

#
# Copyright (c) 2008-2010 Stefan Krah. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#


import sys, os, platform, re
from shutil import copy2
from glob import glob
from distutils import sysconfig
from distutils.core import setup, Extension


DESCRIPTION = """\
Fast arbitrary precision correctly-rounded decimal floating point arithmetic.\
"""

LONG_DESCRIPTION = """
Overview
--------

The ``cdecimal`` package is a fast drop-in replacement for the ``decimal`` module
in Python's standard library. Both modules provide complete implementations of
Mike Cowlishaw/IBM's ``General Decimal Arithmetic Specification``.

Testing
-------

Both ``cdecimal`` and the underlying library - ``libmpdec`` - are extremely
well tested. ``libmpdec`` is one of the few open source projects with 100%
code coverage. ``cdecimal`` is rigorously tested against ``decimal.py``.

Short benchmarks
----------------

Typical performance gains are between 30x for I/O heavy benchmarks
and 80x for numerical programs. In a database benchmark, cdecimal
exhibits a speedup of 12x over decimal.py.

+---------+-------------+--------------+-------------+
|         |   decimal   |   cdecimal   |   speedup   |
+=========+=============+==============+=============+
|   pi    |    42.75s   |    0.58s     |     74x     |
+---------+-------------+--------------+-------------+
| telco   |   172.19s   |    5.68s     |     30x     |
+---------+-------------+--------------+-------------+
| psycopg |     3.57s   |    0.29s     |     12x     |
+---------+-------------+--------------+-------------+

Documentation
-------------

Since ``cdecimal`` is compatible with ``decimal.py``, the official documentation
is valid. For the few remaining differences, refer to the second link.

* `Decimal module <http://docs.python.org/dev/py3k/library/decimal.html>`_
* `Differences between cdecimal and decimal <http://www.bytereef.org/mpdecimal/doc/cdecimal/index.html>`_

Linux Notes
-----------

The build process requires a working C compiler and a *full* Python install with
development headers. Linux distributions often ship the Python header files as
a separate package, called *python-dev* or *python-devel*.

Install headers on Debian/Ubuntu:

* ``sudo apt-get install python-dev``

Windows Notes
-------------

* `Binary installers <http://www.bytereef.org/mpdecimal/download.html>`_

Links
-----

* `cdecimal project homepage <http://www.bytereef.org/mpdecimal/index.html>`_
* `cdecimal benchmarks <http://www.bytereef.org/mpdecimal/benchmarks.html>`_

"""


PY_MAJOR = sys.version_info[0]
PY_MINOR = sys.version_info[1]
ARCH = platform.architecture()[0]
SYSTEM = sys.platform
MACHINE = None

def err_exit(mesg):
    sys.stderr.write("setup.py: error: %s\n" % mesg)
    sys.exit(1)

def uninstall():
    def check_file(name, again=False):
        inputfunc = input if PY_MAJOR >= 3 else raw_input
        if re.search('cdecimal.*\.(so|pyd|egg)', name):
            if again:
                ans = inputfunc("Please answer 'y' or 'n': ")
            else:
                print("Previously installed version detected: %s" % name)
                ans = inputfunc("Remove it [y/n]? ")
            ans = ans.lower()
            if ans == 'y':
                os.unlink(name)
            elif ans != 'n':
                check_file(name, True)
    for p in sys.path:
        if os.path.isdir(p):
            for item in os.listdir(p):
                f = os.path.join(p, item)
                check_file(f)
        elif os.path.isfile(p):
            check_file(p)


if not ((PY_MAJOR == 2 and PY_MINOR >= 5) or PY_MAJOR >= 3):
    err_exit("Python version >= 2.5 required.")
if not (ARCH == '64bit' or ARCH == '32bit'):
    err_exit("64-bit or 32-bit machine required.")

if 'uninstall' in sys.argv:
    uninstall()
    sys.exit(0)


define_macros = []
undef_macros = ['NDEBUG']
if 'build' in sys.argv or 'build_ext' in sys.argv:
    sys.argv.append('--force')
if '--without-threads' in sys.argv:
    # In mpdecimal-1.2, the speed penalty for thread local contexts
    # has been reduced to 8-16%. The default is now to enable thread
    # local contexts. To get the old default (faster), use this option.
    sys.argv.remove('--without-threads')
    define_macros.append(('WITHOUT_THREADS', 1))
else:
    if SYSTEM == 'win32': # also valid for 64-bit.
        try:
            import threading
        except ImportError:
            define_macros.append(('WITHOUT_THREADS', 1))
    elif not sysconfig.get_config_var('WITH_THREAD'):
        define_macros.append(('WITHOUT_THREADS', 1))

# Force specific configuration:
c = ('x64', 'uint128', 'ansi64', 'ppro', 'ansi32', 'ansi-legacy', 'universal')
for i, v in enumerate(sys.argv[:]):
    if '--with-machine=' in v:
        MACHINE = v.split('=')[1]
        sys.argv.pop(i)
        break
if MACHINE and not MACHINE in c:
    err_exit("invalid machine: %s" % MACHINE)

if not MACHINE and 'darwin' in SYSTEM:
    # multi-arch support
    MACHINE = 'universal'

if SYSTEM == 'win32':
    # For automated testing: remove empty string
    sys.argv = [v for v in sys.argv if v != ""]


#
# Generally, it is also possible to build the static libmpdec.a and
# use that to build the Python module. There are systems where this
# approach leads to problems:
#
# Windows: Changes made by Python's setlocale() only affect the
#          state of the dynamically linked CRT. This means that
#          some features of the format() function will not work
#          properly.
#
# AIX:     If built with the static library, the gcc-internals
#          __multi3 and __udivti3 are undefined.
#
MULTILIB_ERR_64 = """
##
## Error: 64-bit Python detected, but ./configure generates 32-bit config.
##
## One of the following choices must be passed to setup.py:
##
##    --with-machine={x64,uint128,ansi64}
##
## OS X only, universal build:
##
##    --with-machine=universal
##
"""
MULTILIB_ERR_32 = """
##
## Error: 32-bit Python detected, but ./configure generates 64-bit config.
##
## One of the following choices must be passed to setup.py:
##
##    --with-machine={ppro,ansi32}
##
## OS X only, universal build:
##
##    --with-machine=universal
##
"""

def configure(machine, cc, py_size_t):
    os.chmod("./configure", 0x1ed) # pip removes execute permissions.
    if machine: # string has been validated.
        os.system("./configure MACHINE=%s" % machine)
    elif 'sunos' in SYSTEM and py_size_t == 8:
        # cc is from sysconfig.
        os.system("./configure CC='%s -m64'" % cc)
    else:
        os.system("./configure")
    f = open("config.h")
    config_vars = {}
    for line in f:
        if line.startswith("#define"):
            l = line.split()
            try:
                config_vars[l[1]] = int(l[2])
            except ValueError:
                pass
        elif line.startswith("/* #undef"):
            l = line.split()
            config_vars[l[2]] = 0
    f.close()
    return config_vars

def cdecimal_ext(machine):
    depends = [
        'basearith.h', 'bits.h', 'constants.h', 'convolute.h', 'crt.h',
        'difradix2.h', 'docstrings.h', 'fnt.h', 'fourstep.h', 'io.h',
        'memory.h', 'mpdecimal.h', 'mpdecimal32.h', 'mpdecimal64.h',
        'mptypes.h', 'numbertheory.h', 'sixstep.h', 'transpose.h',
        'typearith.h', 'umodarith.h'
    ]
    sources = [
        'basearith.c', 'constants.c', 'context.c', 'convolute.c',
        'crt.c', 'difradix2.c', 'fnt.c', 'fourstep.c', 'io.c', 'memory.c',
        'mpdecimal.c', 'numbertheory.c', 'sixstep.c', 'transpose.c'
    ]
    sources.append('cdecimal2.c' if PY_MAJOR == 2 else 'cdecimal3.c')
    extra_compile_args = []
    extra_link_args = []
    extra_objects = []


    if SYSTEM == 'win32':
        define_macros.append(('_CRT_SECURE_NO_WARNINGS', '1'))
        if not machine: # command line option
            if ARCH == '64bit':
                machine = 'x64'
            else:
                machine = 'ppro'

        if machine == 'x64':
            if not (PY_MAJOR == 2 and PY_MINOR == 5):
                # set the build environment for ml64
                from distutils.msvc9compiler import MSVCCompiler
                cc = MSVCCompiler()
                cc.initialize()
                del cc
            define_macros.extend([('CONFIG_64', '1'), ('MASM', '1')])
            extra_objects = ['vcdiv64.obj']
            os.system("ml64 /c /Cx vcdiv64.asm")
            copy2("mpdecimal64vc.h", "mpdecimal.h")
        elif machine == 'ansi64':
            define_macros.extend([('CONFIG_64', '1'), ('ANSI', '1')])
            copy2("mpdecimal64vc.h", "mpdecimal.h")
        elif machine == 'ppro':
            define_macros.extend([('CONFIG_32', '1'), ('PPRO', '1'),
                                  ('MASM', '1')])
            copy2("mpdecimal32vc.h", "mpdecimal.h")
        elif machine == 'ansi32':
            define_macros.extend([('CONFIG_32', '1'), ('ANSI', '1')])
            copy2("mpdecimal32vc.h", "mpdecimal.h")
        elif machine == 'ansi-legacy':
            define_macros.extend([('CONFIG_32', '1'), ('ANSI', '1'),
                                  ('LEGACY_COMPILER', '1')])
            copy2("mpdecimal32vc.h", "mpdecimal.h")
        else:
            err_exit("unsupported machine: %s" % machine)

    else:
        cc = sysconfig.get_config_var('CC')
        py_size_t = sysconfig.get_config_var('SIZEOF_SIZE_T')
        if py_size_t != 8 and py_size_t != 4:
            err_exit("unsupported architecture: sizeof(size_t) must be 8 or 4")

        depends.append('config.h')
        config_vars = configure(machine, cc, py_size_t)
        if config_vars['SIZEOF_SIZE_T'] != 8 and \
           config_vars['SIZEOF_SIZE_T'] != 4:
            err_exit("unsupported architecture: sizeof(size_t) must be 8 or 4")

        if not machine and py_size_t != config_vars['SIZEOF_SIZE_T']:
            if py_size_t == 8:
                print(MULTILIB_ERR_64)
            elif py_size_t == 4:
                print(MULTILIB_ERR_32)
            sys.exit(1)

        if not machine:
            if config_vars['HAVE_GCC_ASM_FOR_X64']:
                machine = 'x64'
            elif config_vars['HAVE_UINT128_T']:
                machine = 'uint128'
            elif config_vars['HAVE_GCC_ASM_FOR_X87']:
                machine = 'ppro'

        if machine == 'universal':
            add_macros = [('UNIVERSAL', '1')]
        elif py_size_t == 8:
            if machine == 'x64':
                add_macros = [('CONFIG_64', '1'), ('ASM', '1')]
            elif machine == 'uint128':
                add_macros = [('CONFIG_64', '1'), ('ANSI', '1'),
                              ('HAVE_UINT128_T', '1')]
            else:
                add_macros = [('CONFIG_64', '1'), ('ANSI', '1')]
        else:
            if machine == 'ppro' and ('gcc' in cc or 'clang' in cc):
                # suncc: register allocator cannot deal with the inline asm.
                # icc >= 11.0 works as well.
                add_macros = [('CONFIG_32', '1'), ('PPRO', '1'), ('ASM', '1')]
                if config_vars['HAVE_IPA_PURE_CONST_BUG']:
                    # Some versions of gcc miscompile inline asm:
                    # http://gcc.gnu.org/bugzilla/show_bug.cgi?id=46491
                    # http://gcc.gnu.org/ml/gcc/2010-11/msg00366.html
                    extra_compile_args.extend(['-fno-ipa-pure-const'])
            else: # ansi32
                add_macros = [('CONFIG_32', '1'), ('ANSI', '1')]
                if machine == 'ansi-legacy':
                    add_macros.append(('LEGACY_COMPILER', '1'))

        # Uncomment for warnings:
        # extra_compile_args.extend(['-Wall', '-W', '-Wno-missing-field-initializers'])

        # Uncomment for a debug build:
        # extra_compile_args.extend(['-O0', '-g'])

        if os.environ.get("CFLAGS"):
            # Distutils drops -fno-strict-aliasing if CFLAGS are set:
            # http://bugs.python.org/issue10847
            if 'xlc' in cc:
                extra_compile_args.append('-qnoansialias')
            else:
                extra_compile_args.append('-fno-strict-aliasing')

        define_macros.extend(add_macros)

        if config_vars['HAVE_GLIBC_MEMMOVE_BUG']:
            # _FORTIFY_SOURCE wrappers for memmove and bcopy are incorrect:
            # http://sourceware.org/ml/libc-alpha/2010-12/msg00009.html
            undef_macros.append('_FORTIFY_SOURCE')

    ext = Extension (
        'cdecimal',
        define_macros=define_macros,
        undef_macros=undef_macros,
        depends=depends,
        extra_compile_args=extra_compile_args,
        extra_link_args=extra_link_args,
        sources=sources,
        extra_objects=extra_objects
    )
    return ext


setup (
    name = 'gryphon-cdecimal',
    version = '2.3',
    description = DESCRIPTION,
    long_description = LONG_DESCRIPTION,
    url = 'http://www.bytereef.org/mpdecimal/index.html',
    author = 'Stefan Krah',
    author_email = 'skrah@bytereef.org',
    license = 'BSD License',
    download_url = 'http://www.bytereef.org/software/mpdecimal/releases/cdecimal-2.3.tar.gz',
    keywords = ["decimal", "floating point", "correctly-rounded",
                "arithmetic", "arbitrary precision"],
    platforms = ["Many"],
    classifiers = [
        "Development Status :: 5 - Production/Stable",
        "Intended Audience :: Developers",
        "Intended Audience :: Education",
        "Intended Audience :: End Users/Desktop",
        "Intended Audience :: Financial and Insurance Industry",
        "Intended Audience :: Science/Research",
        "License :: OSI Approved :: BSD License",
        "Programming Language :: C",
        "Programming Language :: Python :: 2.5",
        "Programming Language :: Python :: 2.6",
        "Programming Language :: Python :: 2.7",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.1",
        "Programming Language :: Python :: 3.2",
        "Operating System :: OS Independent",
        "Topic :: Scientific/Engineering :: Mathematics",
        "Topic :: Software Development"
    ],
    ext_modules = [cdecimal_ext(MACHINE)]
)


PATHLIST = glob("build/lib.*/cdecimal*")
if PATHLIST:
    CDECIMAL = PATHLIST[0]
    copy2(CDECIMAL, "python")



