gdnative_python/site_scons/site_tools/cython.py

160 lines
5.1 KiB
Python

import re
from itertools import takewhile
from SCons.Script import Builder, SharedLibrary
from SCons.Util import CLVar, is_List
from SCons.Errors import UserError
### Cython to C ###
def _cython_to_c_emitter(target, source, env):
if not source:
source = []
elif not is_List(source):
source = [source]
# Consider we always depend on all .pxd files
source += env["CYTHON_DEPS"]
# Add .html target if cython is in annotate mode
if "-a" in env["CYTHON_FLAGS"] or "--annotate" in env["CYTHON_FLAGS"]:
pyx = next(x for x in target if x.name.endswith(".pyx"))
base_name = pyx.get_path().rsplit(".")[0]
return [target[0], f"{base_name}.html"], source
else:
return target, source
CythonToCBuilder = Builder(
action="cython $CYTHON_FLAGS $SOURCE -o $TARGET",
suffix=".c",
src_suffix=".pyx",
emitter=_cython_to_c_emitter,
)
### C compilation to .so ###
def _get_hops_to_site_packages(target):
*parts, _ = target.abspath.split("/")
# Modules installed in `site-packages` come from `pythonscript` folder
return len(list(takewhile(lambda part: part != "pythonscript", reversed(parts))))
def _get_relative_path_to_libpython(env, target):
hops_to_site_packages = _get_hops_to_site_packages(target)
# site_packages is in `<platform>/lib/python3.7/site-packages/`
# and libpython in `<platform>/lib/libpython3.so`
hops_to_libpython_dir = hops_to_site_packages + 2
return "/".join([".."] * hops_to_libpython_dir)
def _get_relative_path_to_libpythonscript(env, target):
hops_to_site_packages = _get_hops_to_site_packages(target)
# site_packages is in `<platform>/lib/python3.7/site-packages/`
# and libpythonscript in `<platform>/libpythonscript.so`
hops_to_libpython_dir = hops_to_site_packages + 3
return "/".join([".."] * hops_to_libpython_dir)
def CythonCompile(env, target, source):
env.Depends(source, env["cpython_build"])
# C code generated by Cython is not *that* clean
if not env["CC_IS_MSVC"]:
cflags = ["-Wno-unused", *env["CFLAGS"]]
else:
cflags = env["CFLAGS"]
# Python native module must have .pyd suffix on windows and .so on POSIX (even on macOS)
if env["platform"].startswith("windows"):
ret = env.SharedLibrary(
target=target,
source=source,
LIBPREFIX="",
SHLIBSUFFIX=".pyd",
CFLAGS=cflags,
LIBS=["python38", "pythonscript"],
# LIBS=[*env["CYTHON_LIBS"], *env["LIBS"]],
# LIBPATH=[*env['CYTHON_LIBPATH'], *env['LIBPATH']]
)
else: # x11 / macos
# Cyton modules depend on libpython.so and libpythonscript.so
# given they won't be available in the default OS lib path we
# must provide their path to the linker
loader_token = "@loader_path" if env["platform"].startswith("osx") else "$$ORIGIN"
libpython_path = _get_relative_path_to_libpython(env, env.File(target))
libpythonscript_path = _get_relative_path_to_libpythonscript(env, env.File(target))
linkflags = [
f"-Wl,-rpath,'{loader_token}/{libpython_path}'",
f"-Wl,-rpath,'{loader_token}/{libpythonscript_path}'",
]
# TODO: use scons `env.LoadableModule` for better macos support ?
ret = env.SharedLibrary(
target=target,
source=source,
LIBPREFIX="",
SHLIBSUFFIX=".so",
CFLAGS=cflags,
LINKFLAGS=[*linkflags, *env["LINKFLAGS"]],
LIBS=["python3.8", "pythonscript"],
# LIBS=[*env["CYTHON_LIBS"], *env["LIBS"]],
# LIBPATH=[*env['CYTHON_LIBPATH'], *env['LIBPATH']]
)
env.Depends(ret, env["CYTHON_COMPILE_DEPS"])
return ret
### Direct Cython to .so ###
def CythonModule(env, target, source=None):
if not target:
target = []
elif not is_List(target):
target = [target]
if not source:
source = []
elif not is_List(source):
source = [source]
# mod_target is passed to the compile builder
mod_target, *other_targets = target
if not source:
source.append(f"{mod_target}.pyx")
pyx_mod, *too_much_mods = [x for x in source if str(x).endswith(".pyx")]
if too_much_mods:
raise UserError(
f"Must have exactly one .pyx file in sources (got `{[mod, *too_much_mods]}`)"
)
c_mod = pyx_mod.split(".", 1)[0] + ".c" # Useful to do `xxx.gen.pyx` ==> `xxx`
CythonToCBuilder(env, target=[c_mod, *other_targets], source=source)
c_compile_target = CythonCompile(env, target=mod_target, source=[c_mod])
return [*c_compile_target, *other_targets]
### Scons tool hooks ###
def generate(env):
"""Add Builders and construction variables for ar to an Environment."""
env["CYTHON_FLAGS"] = CLVar("--fast-fail -3")
env["CYTHON_DEPS"] = []
env["CYTHON_COMPILE_DEPS"] = []
env.Append(BUILDERS={"CythonToC": CythonToCBuilder})
env.AddMethod(CythonCompile, "CythonCompile")
env.AddMethod(CythonModule, "CythonModule")
def exists(env):
return env.Detect("cython")