Files
cantera/interfaces/cython/SConscript
Bryan Weber 5567c71f19 [SCons/Cython] Use pip to build/install interface
Setuptools versions greater than 60.0.0 deprecate using setup.py install
commands, in preference for PEP 517/518 build backends/frontends. In
this case, we're using pip as the frontend and setuptools as the
backend.

This also uses pip to determine the installation location for the
module. It uses pip's internal API which is subject to change. However,
pip does not expose an external API to get this information, and since
Pip is the one determining where files will be installed, this seems the
most reasonable approach for now.
2022-03-23 13:38:09 -04:00

201 lines
7.9 KiB
Python

"""Cython-based Python Module"""
from __future__ import annotations
import re
from pathlib import Path
from pkg_resources import parse_version
import json
from buildutils import *
from textwrap import dedent
Import('env', 'build', 'install')
localenv = env.Clone()
cythonized = localenv.Command(
'cantera/_cantera.cpp',
'cantera/_cantera.pyx',
'''${python_cmd} -c "import Cython.Build; Cython.Build.cythonize('${SOURCE}')"''')
for f in multi_glob(localenv, 'cantera', 'pyx', 'pxd'):
localenv.Depends(cythonized, f)
for line in Path(File('#interfaces/cython/cantera/_cantera.pxd').abspath).read_text().splitlines():
m = re.search(r'from "(cantera.*?)"', line)
if m:
localenv.Depends('cantera/_cantera.cpp', '#include/' + m.group(1))
dataFiles = localenv.RecursiveInstall('cantera/data',
'#build/data')
build(dataFiles)
testFiles = localenv.RecursiveInstall('cantera/test/data',
'#test/data')
build(testFiles)
# Get information needed to build the Python module
script = """\
from sysconfig import *
import numpy
import json
vars = get_config_vars()
vars["plat"] = get_platform()
vars["numpy_include"] = numpy.get_include()
print(json.dumps(vars))
"""
info = json.loads(get_command_output(localenv["python_cmd"], "-c", script))
module_ext = info["EXT_SUFFIX"]
inc = info["INCLUDEPY"]
pylib = info.get("LDLIBRARY")
prefix = info["prefix"]
py_version_short = parse_version(info["py_version_short"])
py_version_full = parse_version(info["py_version"])
py_version_nodot = info['py_version_nodot']
numpy_include = info["numpy_include"]
localenv.Prepend(CPPPATH=[Dir('#include'), inc, numpy_include])
localenv.Prepend(LIBS=localenv['cantera_libs'])
# Fix the module extension for Windows from the sysconfig library.
# See https://github.com/python/cpython/pull/22088 and
# https://bugs.python.org/issue39825
if (
py_version_full < parse_version("3.8.7")
and localenv["OS"] == "Windows"
and module_ext == ".pyd"
):
module_ext = f".cp{py_version_nodot}-{info['plat'].replace('-', '_')}.pyd"
# Don't print deprecation warnings for internal Python changes.
# Only applies to Python 3.8. The field that is deprecated in Python 3.8
# and causes the warnings to appear will be removed in Python 3.9 so no
# further warnings should be issued.
if localenv["HAS_CLANG"] and py_version_short == parse_version("3.8"):
localenv.Append(CXXFLAGS='-Wno-deprecated-declarations')
if "icc" in localenv["CC"]:
localenv.Append(CPPDEFINES={"CYTHON_FALLTHROUGH":" __attribute__((fallthrough))"})
if localenv['OS'] == 'Darwin':
localenv.Append(LINKFLAGS='-undefined dynamic_lookup')
elif localenv['OS'] == 'Windows':
localenv.Append(LIBPATH=prefix+'/libs')
if localenv['toolchain'] == 'mingw':
localenv.Append(LIBS=f"python{py_version_nodot}")
if localenv['OS_BITS'] == 64:
localenv.Append(CPPDEFINES='MS_WIN64')
# Fix for https://bugs.python.org/issue11566. Fixed in 3.7.3 and higher.
# See https://github.com/python/cpython/pull/11283
if py_version_full < parse_version("3.7.3"):
localenv.Append(CPPDEFINES={"_hypot": "hypot"})
elif localenv['OS'] == 'Cygwin':
# extract 'pythonX.Y' from 'libpythonX.Y.dll.a'
localenv.Append(LIBS=pylib[3:-6])
localenv["module_ext"] = module_ext
setup_cfg = localenv.SubstFile("setup.cfg", "setup.cfg.in")
readme = localenv.Command("README.rst", "#README.rst", Copy("$TARGET", "$SOURCE"))
license = localenv.Command("LICENSE.txt", "#build/ext/LICENSE.txt",
Copy("$TARGET", "$SOURCE"))
localenv.Depends(license, localenv["license_target"])
# Build the Python module
obj = localenv.SharedObject('#build/temp-py/_cantera', 'cantera/_cantera.cpp')
ext = localenv.LoadableModule(f"cantera/_cantera{module_ext}",
obj, LIBPREFIX="", SHLIBSUFFIX=module_ext,
SHLIBPREFIX="", LIBSUFFIXES=[module_ext])
build_cmd = ("$python_cmd_esc -m pip wheel --no-build-isolation --no-deps "
"--wheel-dir=build/python/dist build/python")
wheel_name = (f"Cantera-{env['cantera_version']}-cp{py_version_nodot}"
f"-cp{py_version_nodot}-{info['plat'].replace('-', '_')}.whl")
mod = build(localenv.Command(f"#build/python/dist/{wheel_name}", "setup.cfg", build_cmd))
env['python_module'] = mod
env['python_extension'] = ext
localenv.Depends(mod, [ext, dataFiles, testFiles, setup_cfg, readme, license,
"setup.py", "pyproject.toml"])
localenv.Depends(ext, localenv['cantera_staticlib'])
for f in (multi_glob(localenv, 'cantera', 'py') +
multi_glob(localenv, 'cantera/*', 'py') +
multi_glob(localenv, 'cantera/*/*', 'py')):
localenv.Depends(mod, f)
# Determine installation path and install the Python module
install_cmd = ["$python_cmd_esc", "-m", "pip", "install"]
def get_install_location(
user: bool = False,
prefix: str | None = None,
root: str | None = None
) -> dict[str, str]:
"""Determine the location where pip will install files.
This relies on pip's internal API so it may break in future versions.
Unfortunately, I don't really see another way to determine this information
reliably.
"""
# These need to be quoted if they're not None, even if they're a falsey value
# like the empty string. Otherwise, we want the literal None value.
prefix = quoted(prefix) if prefix is not None else None
root = quoted(root) if root is not None else None
install_script = dedent(f"""
from pip import __version__ as pip_version
from pkg_resources import parse_version
import pip
import json
pip_version = parse_version(pip_version)
if pip_version < parse_version("10.0.0"):
from pip.locations import distutils_scheme
scheme = distutils_scheme("Cantera", user={user}, root={root},
prefix={prefix})
else:
from pip._internal.locations import get_scheme
scheme = get_scheme("Cantera", user={user}, root={root},
prefix={prefix})
if not isinstance(scheme, dict):
scheme = {{k: getattr(scheme, k) for k in dir(scheme)
if not k.startswith("_")}}
scheme["pip"] = pip.__file__
print(json.dumps(scheme))
""")
return json.loads(get_command_output(localenv["python_cmd"], "-c", install_script))
user_install = False
python_prefix = None
python_root = None
if localenv['python_prefix'] == 'USER':
# Install to the OS-dependent user site-packages directory
install_cmd.append("--user")
user_install = True
elif localenv['python_prefix']:
install_cmd.append(f"--prefix={localenv.subst('python_prefix')}")
python_prefix = localenv.subst("python_prefix")
if env["stage_dir"]:
# Get the absolute path to the stage directory. If the stage directory is a relative
# path, consider it to be relative to the root of the Cantera source directory.
stage_dir = Path(env["stage_dir"])
if not stage_dir.is_absolute():
stage_dir = Path(Dir("#").abspath) / stage_dir
install_cmd.append(f"--root={stage_dir.resolve()}")
python_root = stage_dir.resolve()
install_cmd.extend(("--no-build-isolation", "--no-deps", "-v", "--force-reinstall", "build/python"))
if localenv['PYTHON_INSTALLER'] == 'direct':
mod_inst = install(localenv.Command, 'dummy', mod,
" ".join(install_cmd))
env["install_python_action"] = mod_inst
install_locs = get_install_location(user_install, python_prefix, python_root)
env["python_module_loc"] = install_locs["platlib"]
env["ct_pyscriptdir"] = install_locs["scripts"]
elif localenv['PYTHON_INSTALLER'] == 'debian':
install(localenv.Command, 'dummy', mod,
'cd build/python && '
'$python_cmd_esc setup.py build --build-lib=. '
'install --install-layout=deb --no-compile --root=${python_prefix}')
env["python_module_loc"] = "<unspecified>"