1"""Build a Pyrex file from .pyx source to .so loadable module using
2the installed distutils infrastructure. Call:
3
4out_fname = pyx_to_dll("foo.pyx")
5"""
6import os
7import sys
8
9from distutils.errors import DistutilsArgError, DistutilsError, CCompilerError
10from distutils.extension import Extension
11from distutils.util import grok_environment_error
12try:
13    from Cython.Distutils.old_build_ext import old_build_ext as build_ext
14    HAS_CYTHON = True
15except ImportError:
16    HAS_CYTHON = False
17
18DEBUG = 0
19
20_reloads={}
21
22
23def pyx_to_dll(filename, ext=None, force_rebuild=0, build_in_temp=False, pyxbuild_dir=None,
24               setup_args=None, reload_support=False, inplace=False):
25    """Compile a PYX file to a DLL and return the name of the generated .so
26       or .dll ."""
27    assert os.path.exists(filename), "Could not find %s" % os.path.abspath(filename)
28
29    path, name = os.path.split(os.path.abspath(filename))
30
31    if not ext:
32        modname, extension = os.path.splitext(name)
33        assert extension in (".pyx", ".py"), extension
34        if not HAS_CYTHON:
35            filename = filename[:-len(extension)] + '.c'
36        ext = Extension(name=modname, sources=[filename])
37
38    if setup_args is None:
39        setup_args = {}
40    if not pyxbuild_dir:
41        pyxbuild_dir = os.path.join(path, "_pyxbld")
42
43    package_base_dir = path
44    for package_name in ext.name.split('.')[-2::-1]:
45        package_base_dir, pname = os.path.split(package_base_dir)
46        if pname != package_name:
47            # something is wrong - package path doesn't match file path
48            package_base_dir = None
49            break
50
51    script_args=setup_args.get("script_args",[])
52    if DEBUG or "--verbose" in script_args:
53        quiet = "--verbose"
54    else:
55        quiet = "--quiet"
56    args = [quiet, "build_ext"]
57    if force_rebuild:
58        args.append("--force")
59    if inplace and package_base_dir:
60        args.extend(['--build-lib', package_base_dir])
61        if ext.name == '__init__' or ext.name.endswith('.__init__'):
62            # package => provide __path__ early
63            if not hasattr(ext, 'cython_directives'):
64                ext.cython_directives = {'set_initial_path' : 'SOURCEFILE'}
65            elif 'set_initial_path' not in ext.cython_directives:
66                ext.cython_directives['set_initial_path'] = 'SOURCEFILE'
67
68    if HAS_CYTHON and build_in_temp:
69        args.append("--pyrex-c-in-temp")
70    sargs = setup_args.copy()
71    sargs.update({
72        "script_name": None,
73        "script_args": args + script_args,
74    })
75    # late import, in case setuptools replaced it
76    from distutils.dist import Distribution
77    dist = Distribution(sargs)
78    if not dist.ext_modules:
79        dist.ext_modules = []
80    dist.ext_modules.append(ext)
81    if HAS_CYTHON:
82        dist.cmdclass = {'build_ext': build_ext}
83    build = dist.get_command_obj('build')
84    build.build_base = pyxbuild_dir
85
86    cfgfiles = dist.find_config_files()
87    dist.parse_config_files(cfgfiles)
88
89    try:
90        ok = dist.parse_command_line()
91    except DistutilsArgError:
92        raise
93
94    if DEBUG:
95        print("options (after parsing command line):")
96        dist.dump_option_dicts()
97    assert ok
98
99
100    try:
101        obj_build_ext = dist.get_command_obj("build_ext")
102        dist.run_commands()
103        so_path = obj_build_ext.get_outputs()[0]
104        if obj_build_ext.inplace:
105            # Python distutils get_outputs()[ returns a wrong so_path
106            # when --inplace ; see http://bugs.python.org/issue5977
107            # workaround:
108            so_path = os.path.join(os.path.dirname(filename),
109                                   os.path.basename(so_path))
110        if reload_support:
111            org_path = so_path
112            timestamp = os.path.getmtime(org_path)
113            global _reloads
114            last_timestamp, last_path, count = _reloads.get(org_path, (None,None,0) )
115            if last_timestamp == timestamp:
116                so_path = last_path
117            else:
118                basename = os.path.basename(org_path)
119                while count < 100:
120                    count += 1
121                    r_path = os.path.join(obj_build_ext.build_lib,
122                                          basename + '.reload%s'%count)
123                    try:
124                        import shutil # late import / reload_support is: debugging
125                        try:
126                            # Try to unlink first --- if the .so file
127                            # is mmapped by another process,
128                            # overwriting its contents corrupts the
129                            # loaded image (on Linux) and crashes the
130                            # other process. On Windows, unlinking an
131                            # open file just fails.
132                            if os.path.isfile(r_path):
133                                os.unlink(r_path)
134                        except OSError:
135                            continue
136                        shutil.copy2(org_path, r_path)
137                        so_path = r_path
138                    except IOError:
139                        continue
140                    break
141                else:
142                    # used up all 100 slots
143                    raise ImportError("reload count for %s reached maximum"%org_path)
144                _reloads[org_path]=(timestamp, so_path, count)
145        return so_path
146    except KeyboardInterrupt:
147        sys.exit(1)
148    except (IOError, os.error):
149        exc = sys.exc_info()[1]
150        error = grok_environment_error(exc)
151
152        if DEBUG:
153            sys.stderr.write(error + "\n")
154        raise
155
156
157if __name__=="__main__":
158    pyx_to_dll("dummy.pyx")
159    from . import test
160
161