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