pyxbuild.py 5.44 KB
Newer Older
1 2 3 4 5
"""Build a Pyrex file from .pyx source to .so loadable module using
the installed distutils infrastructure. Call:

out_fname = pyx_to_dll("foo.pyx")
"""
Stefan Behnel's avatar
Stefan Behnel committed
6
import os
7
import sys
8 9 10 11 12

from distutils.dist import Distribution
from distutils.errors import DistutilsArgError, DistutilsError, CCompilerError
from distutils.extension import Extension
from distutils.util import grok_environment_error
13 14 15 16 17
try:
    from Cython.Distutils import build_ext
    HAS_CYTHON = True
except ImportError:
    HAS_CYTHON = False
18 19

DEBUG = 0
20 21 22

_reloads={}

23
def pyx_to_dll(filename, ext = None, force_rebuild = 0,
24 25
               build_in_temp=False, pyxbuild_dir=None, setup_args={},
               reload_support=False, inplace=False):
26 27
    """Compile a PYX file to a DLL and return the name of the generated .so 
       or .dll ."""
28
    assert os.path.exists(filename), "Could not find %s" % os.path.abspath(filename)
29

30
    path, name = os.path.split(os.path.abspath(filename))
31 32 33

    if not ext:
        modname, extension = os.path.splitext(name)
34
        assert extension in (".pyx", ".py"), extension
35 36
        if not HAS_CYTHON:
            filename = filename[:-len(extension)] + '.c'
37 38
        ext = Extension(name=modname, sources=[filename])

39 40 41
    if not pyxbuild_dir:
        pyxbuild_dir = os.path.join(path, "_pyxbld")

42
    package_base_dir = path
43
    for package_name in ext.name.split('.')[-2::-1]:
44 45 46 47 48 49
        package_base_dir, pname = os.path.split(package_base_dir)
        if pname != package_name:
            # something is wrong - package path doesn't match file path
            package_base_dir = None
            break

50 51
    script_args=setup_args.get("script_args",[])
    if DEBUG or "--verbose" in script_args:
52 53
        quiet = "--verbose"
    else:
54
        quiet = "--quiet"
55 56 57
    args = [quiet, "build_ext"]
    if force_rebuild:
        args.append("--force")
58
    if inplace and package_base_dir:
Stefan Behnel's avatar
Stefan Behnel committed
59
        args.extend(['--build-lib', package_base_dir])
60 61 62
        if ext.name == '__init__' or ext.name.endswith('.__init__'):
            # package => provide __path__ early
            if not hasattr(ext, 'cython_directives'):
63 64 65
                ext.cython_directives = {'set_initial_path' : 'SOURCEFILE'}
            elif 'set_initial_path' not in ext.cython_directives:
                ext.cython_directives['set_initial_path'] = 'SOURCEFILE'
66

67
    if HAS_CYTHON and build_in_temp:
68
        args.append("--pyrex-c-in-temp")
69 70 71 72 73
    sargs = setup_args.copy()
    sargs.update(
        {"script_name": None,
         "script_args": args + script_args} )
    dist = Distribution(sargs)
74 75 76
    if not dist.ext_modules:
        dist.ext_modules = []
    dist.ext_modules.append(ext)
77 78
    if HAS_CYTHON:
        dist.cmdclass = {'build_ext': build_ext}
79
    build = dist.get_command_obj('build')
80
    build.build_base = pyxbuild_dir
81

82 83
    cfgfiles = dist.find_config_files()
    dist.parse_config_files(cfgfiles)
84

85 86
    try:
        ok = dist.parse_command_line()
87
    except DistutilsArgError:
88 89 90
        raise

    if DEBUG:
91
        print("options (after parsing command line):")
92 93 94 95 96
        dist.dump_option_dicts()
    assert ok


    try:
97
        obj_build_ext = dist.get_command_obj("build_ext")
98
        dist.run_commands()
99
        so_path = obj_build_ext.get_outputs()[0]
100
        if obj_build_ext.inplace:
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
            # Python distutils get_outputs()[ returns a wrong so_path 
            # when --inplace ; see http://bugs.python.org/issue5977
            # workaround:
            so_path = os.path.join(os.path.dirname(filename),
                                   os.path.basename(so_path))
        if reload_support:
            org_path = so_path
            timestamp = os.path.getmtime(org_path)
            global _reloads
            last_timestamp, last_path, count = _reloads.get(org_path, (None,None,0) )
            if last_timestamp == timestamp:
                so_path = last_path
            else:
                basename = os.path.basename(org_path)
                while count < 100:
                    count += 1
                    r_path = os.path.join(obj_build_ext.build_lib,
                                          basename + '.reload%s'%count)
                    try:
                        import shutil # late import / reload_support is: debugging
121 122 123 124 125 126 127 128 129 130 131
                        try:
                            # Try to unlink first --- if the .so file
                            # is mmapped by another process,
                            # overwriting its contents corrupts the
                            # loaded image (on Linux) and crashes the
                            # other process. On Windows, unlinking an
                            # open file just fails.
                            if os.path.isfile(r_path):
                                os.unlink(r_path)
                        except OSError:
                            continue
132 133 134 135 136 137 138 139 140 141
                        shutil.copy2(org_path, r_path)
                        so_path = r_path
                    except IOError:
                        continue
                    break
                else:
                    # used up all 100 slots 
                    raise ImportError("reload count for %s reached maximum"%org_path)
                _reloads[org_path]=(timestamp, so_path, count)
        return so_path
142
    except KeyboardInterrupt:
143 144 145
        sys.exit(1)
    except (IOError, os.error):
        exc = sys.exc_info()[1]
146 147 148 149
        error = grok_environment_error(exc)

        if DEBUG:
            sys.stderr.write(error + "\n")
150
        raise
151 152 153 154 155

if __name__=="__main__":
    pyx_to_dll("dummy.pyx")
    import test