import os, re, sys
from distutils import util, sysconfig
from distutils.command import build_ext
from distutils.dep_util import newer_group, newer

# Constants for symbol stripping
STRIP_NONE = 0
STRIP_VERSIONING = 1
STRIP_EXPORTS_FILE = 2
STRIP_EXPORTS_ARGLIST = 3
STRIP_EXPORTS_POST_LINK = 4

class BuildExt(build_ext.build_ext):

    command_name = 'build_ext'

    def initialize_options(self):
        build_ext.build_ext.initialize_options(self)

        # How to format C symbol name to exported symbol name
        self.export_symbol_format = '%s'

        self.symbol_stripping = STRIP_NONE
        self.strip_command = None
        return
    
    def finalize_options(self):
        build_ext.build_ext.finalize_options(self)

        # OpenBSD and NetBSD dlsyms have a leading underscore if the object
        # format is not ELF.  (from src/Python/dynload_shlib.c).
        if (sys.platform.startswith('openbsd')
            or sys.platform.startswith('netbsd')):
            # Capture predefined preprocessor macros (from src/configure)
            cc = sysconfig.get_config_var('CC')
            defines = os.popen(cc + ' -dM -E - </dev/null').read()
            # Check for ELF object format
            if defines.find('__ELF__') == -1:
                self.export_symbol_format = '_%s'

        # By limiting exported symbols, we don't need to worry about symbol
        # conflicts between the shared modules or Python itself.
        if os.name == 'nt' or sys.platform.startswith('cygwin'):
            # The compiler default is to limit exported symbols
            self.symbol_stripping = STRIP_NONE

        elif (sys.platform.startswith('linux')
              or sys.platform.startswith('freebsd')
              or sys.platform.startswith('openbsd')
              or sys.platform.startswith('netbsd')):
            # This assumes the the GNU linker is being used.
            # As of Dec 2005, the SourceForge Compile Farm servers use GNU ld
            # for OpenBSD and NetBSD.
            self.symbol_stripping = STRIP_VERSIONING
            self.strip_command = '-Wl,--version-script,%s'

        elif sys.platform.startswith('sunos'):
            self.symbol_stripping = STRIP_VERSIONING
            self.strip_command = '-Wl,-M,%s'

        elif sys.platform.startswith('darwin'):
            # Mac OS X/Darwin
            ld = sysconfig.get_config_var('LDSHARED')
            output = os.popen(ld + ' -Wl,-exported_symbols_list').read()
            if re.search('unknown flag: -exported_symbols_list', output):
                # Older OSX (10.1 or 10.2 with DevTools prior to Dec 2002)
                # Use external program (nmedit) to limit exported symbols
                self.symbol_stripping = STRIP_EXPORTS_POST_LINK
                self.strip_command = 'nmedit -s %(exports)s -p %(extension)s'
            else:
                self.symbol_stripping = STRIP_EXPORTS_FILE
                self.strip_command = '-Wl,-exported_symbols_list,%s'
                self.export_symbol_format = '_%s'

        elif sys.platform.startswith('hp-ux'):
            # HP-UX linker lists exported symbols one at a time in the
            # argument list.
            self.symbol_stripping = STRIP_EXPORTS_ARGLIST
            self.strip_command = '-Wl,+e,%s'

        elif os.name == 'posix':
            # From online manual pages, most UNIX support limiting exported
            # symbols with the same option.
            self.symbol_stripping = STRIP_EXPORTS_FILE
            self.strip_command = '-Wl,-exports_file,%s'
        return

    _include_re = re.compile(r'^\s*#\s*include\s*("|<)(?P<file>[^">]+?)("|>)',
                             re.MULTILINE)

    def _find_includes(self, ext, filename, includes):
        sourcefile = open(filename)
        source = sourcefile.read()
        sourcefile.close()
        basename = os.path.basename(filename)

        include_dirs = [os.path.dirname(filename)]
        include_dirs.extend(ext.include_dirs)

        match = self._include_re.search(source)
        while match:
            filename = util.convert_path(match.group('file'))
            for base in include_dirs:
                include = os.path.normpath(os.path.join(base, filename))
                if os.path.isfile(include) and include not in includes:
                    includes.append(include)
                    self._find_includes(ext, include, includes)
                    break
            match = self._include_re.search(source, match.end())
        return

    def check_extensions_list(self, extensions):
        build_ext.build_ext.check_extensions_list(self, extensions)

        # Add the included files for each source file
        for ext in extensions:
            ext.includes = []
            for source in ext.sources:
                includes = []
                self._find_includes(ext, util.convert_path(source), includes)
                ext.includes.append(includes)
        return

    def get_source_files (self):
        self.check_extensions_list(self.extensions)

        filenames = []
        for ext in self.extensions:
            filenames.extend(ext.sources)
            for includes in ext.includes:
                filenames.extend(includes)
        return filenames

    def build_extension(self, ext):
        fullname = self.get_ext_fullname(ext.name)
        ext_filename = os.path.join(self.build_lib,
                                    self.get_ext_filename(fullname))

        sourcefile = __file__
        if sourcefile[:-4] in ('.pyc', '.pyo'):
            sourcefile = sourcefile[:-4] + '.py'
        depends = [sourcefile]
        depends.extend(ext.sources)
        if sys.hexversion >= 0x02030000:
            depends.extend(ext.depends)
        for includes in ext.includes:
            depends.extend(includes)

        if not (self.force or newer_group(depends, ext_filename, 'newer')):
            self.announce("skipping '%s' extension (up-to-date)" % ext.name)
            return

        self.announce("building '%s' extension" % ext.name, 2)

        extra_args = ext.extra_compile_args or []
        
        macros = ext.define_macros[:]
        for undef in ext.undef_macros:
            macros.append((undef,))

        # Get the resulting object filenames as we are compiling the sources
        # one at a time to reduce compile time for large source lists.
        objects = self.compiler.object_filenames(ext.sources,
                                                 sysconfig.python_build,
                                                 self.build_temp)

        sourcefile = __file__
        if sourcefile[:-4] in ('.pyc', '.pyo'):
            sourcefile = sourcefile[:-4] + '.py'
        if sys.hexversion >= 0x02030000:
            # Python 2.3 added dependency checking to the compiler, use that
            for source, includes in zip(ext.sources, ext.includes):
                depends = [sourcefile]
                depends.extend(ext.depends)
                depends.extend(includes)
                self.compiler.compile([source],
                                      output_dir=self.build_temp,
                                      macros=macros,
                                      include_dirs=ext.include_dirs,
                                      debug=self.debug,
                                      extra_postargs=extra_args,
                                      depends=depends)
        else:
            if not self.force:
                # Determine those sources that require rebuilding
                sources = []
                for object, source, includes in zip(objects, ext.sources,
                                                    ext.includes):
                    depends = [sourcefile, source]
                    depends.extend(includes)
                    if newer_group(depends, object, 'newer'):
                        sources.append(source)
            else:
                # Rebuild all sources
                sources = ext.sources

            # Forcably build those sources listed in 'sources'
            self.compiler.force = 1
            for source in sources:
                output_dir = os.path.join(self.build_temp,
                                          os.path.dirname(source))
                self.compiler.compile([source],
                                      output_dir=output_dir,
                                      macros=macros,
                                      include_dirs=ext.include_dirs,
                                      debug=self.debug,
                                      extra_postargs=extra_args)

            # Reset the force flag on the compilier
            self.compiler.force = self.force
        
        # Now link the object files together into a "shared object" --
        # of course, first we have to figure out all the other things
        # that go into the mix.
        if ext.extra_objects:
            objects.extend(ext.extra_objects)

        # Setup "symbol stripping"
        if self.symbol_stripping == STRIP_VERSIONING:
            # Strip symbols via a versioning script
            f, mapfile = self._mkstemp(ext, '.map')
            f.write('{ global: ')
            for sym in self.get_export_symbols(ext):
                f.write(sym + '; ')
            f.write('local: *; };')
            f.close()
            link_preargs = [self.strip_command % mapfile]

        elif self.symbol_stripping == STRIP_EXPORTS_FILE:
            # Strip symbols via an exports file
            f, expfile = self._mkstemp(ext, '.exp')
            for sym in self.get_export_symbols(ext):
                f.write(sym + '\n')
            f.close()
            link_preargs = [self.strip_command % expfile]

        elif self.symbol_stripping == STRIP_EXPORTS_ARGLIST:
            # Strip symbols via multiple arguments
            symbols = self.get_export_symbols(ext)
            link_preargs = [ self.strip_command % sym for sym in symbols ]

        else:
            # No linker support for limiting exported symbols
            link_preargs = []

        if sys.hexversion < 0x02030000:
            kwords = {}
        else:
            # Detect target language, if not provided
            lang = ext.language or self.compiler.detect_language(ext.sources)
            kwords = {'target_lang' : lang}

        self.compiler.link_shared_object(
            objects, ext_filename,
            libraries=self.get_libraries(ext),
            library_dirs=ext.library_dirs,
            runtime_library_dirs=ext.runtime_library_dirs,
            extra_preargs=link_preargs,
            extra_postargs=ext.extra_link_args,
            export_symbols=self.get_export_symbols(ext),
            debug=self.debug,
            build_temp=self.build_temp,
            **kwords)

        if self.symbol_stripping == STRIP_EXPORTS_POST_LINK:
            # Create the exports file
            f, expfile = self._mkstemp(ext, '.exp')
            for sym in self.get_export_symbols(ext):
                f.write(sym + '\n')
            f.close()

            subst = {'exports' : expfile, 'extension' : filename}
            self.spawn([ x % subst for x in self.strip_command.split(' ') ])
        return

    def _mkstemp(self, extension, suffix):
        path_parts = extension.name.split('.')
        basename = os.path.join(self.build_temp, *path_parts)
        # extensions in debug_mode are named 'module_d.pyd' under windows
        if os.name == 'nt' and self.debug:
            basename += '_d'
        filename = basename + suffix
        self.mkpath(os.path.dirname(filename))
        return (open(filename, 'w'), filename)

    def get_export_symbols(self, extension):
        symbols = build_ext.build_ext.get_export_symbols(self, extension)
        return [ self.export_symbol_format % symbol for symbol in symbols ]
