1"""
2Compile a Python script into an executable that embeds CPython and run it.
3Requires CPython to be built as a shared library ('libpythonX.Y').
4
5Basic usage:
6
7    python cythonrun somefile.py [ARGS]
8"""
9
10DEBUG = True
11
12import sys
13import os
14from distutils import sysconfig
15
16def get_config_var(name, default=''):
17    return sysconfig.get_config_var(name) or default
18
19INCDIR = sysconfig.get_python_inc()
20LIBDIR1 = get_config_var('LIBDIR')
21LIBDIR2 = get_config_var('LIBPL')
22PYLIB = get_config_var('LIBRARY')
23PYLIB_DYN = get_config_var('LDLIBRARY')
24if PYLIB_DYN == PYLIB:
25    # no shared library
26    PYLIB_DYN = ''
27else:
28    PYLIB_DYN = os.path.splitext(PYLIB_DYN[3:])[0] # 'lib(XYZ).so' -> XYZ
29
30CC = get_config_var('CC', os.environ.get('CC', ''))
31CFLAGS = get_config_var('CFLAGS') + ' ' + os.environ.get('CFLAGS', '')
32LINKCC = get_config_var('LINKCC', os.environ.get('LINKCC', CC))
33LINKFORSHARED = get_config_var('LINKFORSHARED')
34LIBS = get_config_var('LIBS')
35SYSLIBS = get_config_var('SYSLIBS')
36EXE_EXT = sysconfig.get_config_var('EXE')
37
38def _debug(msg, *args):
39    if DEBUG:
40        if args:
41            msg = msg % args
42        sys.stderr.write(msg + '\n')
43
44def dump_config():
45    _debug('INCDIR: %s', INCDIR)
46    _debug('LIBDIR1: %s', LIBDIR1)
47    _debug('LIBDIR2: %s', LIBDIR2)
48    _debug('PYLIB: %s', PYLIB)
49    _debug('PYLIB_DYN: %s', PYLIB_DYN)
50    _debug('CC: %s', CC)
51    _debug('CFLAGS: %s', CFLAGS)
52    _debug('LINKCC: %s', LINKCC)
53    _debug('LINKFORSHARED: %s', LINKFORSHARED)
54    _debug('LIBS: %s', LIBS)
55    _debug('SYSLIBS: %s', SYSLIBS)
56    _debug('EXE_EXT: %s', EXE_EXT)
57
58def runcmd(cmd, shell=True):
59    if shell:
60        cmd = ' '.join(cmd)
61        _debug(cmd)
62    else:
63        _debug(' '.join(cmd))
64
65    try:
66        import subprocess
67    except ImportError: # Python 2.3 ...
68        returncode = os.system(cmd)
69    else:
70        returncode = subprocess.call(cmd, shell=shell)
71
72    if returncode:
73        sys.exit(returncode)
74
75def clink(basename):
76    runcmd([LINKCC, '-o', basename + EXE_EXT, basename+'.o', '-L'+LIBDIR1, '-L'+LIBDIR2]
77           + [PYLIB_DYN and ('-l'+PYLIB_DYN) or os.path.join(LIBDIR1, PYLIB)]
78           + LIBS.split() + SYSLIBS.split() + LINKFORSHARED.split())
79
80def ccompile(basename):
81    runcmd([CC, '-c', '-o', basename+'.o', basename+'.c', '-I' + INCDIR] + CFLAGS.split())
82
83def cycompile(input_file, options=()):
84    from Cython.Compiler import Version, CmdLine, Main
85    options, sources = CmdLine.parse_command_line(list(options or ()) + ['--embed', input_file])
86    _debug('Using Cython %s to compile %s', Version.version, input_file)
87    result = Main.compile(sources, options)
88    if result.num_errors > 0:
89        sys.exit(1)
90
91def exec_file(program_name, args=()):
92    runcmd([os.path.abspath(program_name)] + list(args), shell=False)
93
94def build(input_file, compiler_args=(), force=False):
95    """
96    Build an executable program from a Cython module.
97
98    Returns the name of the executable file.
99    """
100    basename = os.path.splitext(input_file)[0]
101    exe_file = basename + EXE_EXT
102    if not force and os.path.abspath(exe_file) == os.path.abspath(input_file):
103        raise ValueError("Input and output file names are the same, refusing to overwrite")
104    if (not force and os.path.exists(exe_file) and os.path.exists(input_file)
105        and os.path.getmtime(input_file) <= os.path.getmtime(exe_file)):
106        _debug("File is up to date, not regenerating %s", exe_file)
107        return exe_file
108    cycompile(input_file, compiler_args)
109    ccompile(basename)
110    clink(basename)
111    return exe_file
112
113def build_and_run(args):
114    """
115    Build an executable program from a Cython module and runs it.
116
117    Arguments after the module name will be passed verbatimely to the
118    program.
119    """
120    cy_args = []
121    last_arg = None
122    for i, arg in enumerate(args):
123        if arg.startswith('-'):
124            cy_args.append(arg)
125        elif last_arg in ('-X', '--directive'):
126            cy_args.append(arg)
127        else:
128            input_file = arg
129            args = args[i+1:]
130            break
131        last_arg = arg
132    else:
133        raise ValueError('no input file provided')
134
135    program_name = build(input_file, cy_args)
136    exec_file(program_name, args)
137
138if __name__ == '__main__':
139    build_and_run(sys.argv[1:])
140