Files

256 lines
11 KiB
Python

"""Cython-based Python Module"""
import re
from pathlib import Path
from packaging.specifiers import SpecifierSet
from packaging.version import parse as parse_version
from buildutils import (get_command_output, which, multi_glob, logger,
get_pip_install_location, setup_python_env)
Import('env', 'build', 'install')
localenv = env.Clone()
dataFiles = []
for yaml in multi_glob(localenv, "#data", "yaml"):
dataFiles.append(build(
localenv.Command(f"cantera/data/{yaml.name}", yaml.abspath,
Copy("$TARGET", "$SOURCE"))))
if localenv["example_data"]:
for yaml in multi_glob(localenv, "#data/example_data", "yaml"):
dataFiles.append(build(
localenv.Command(f"cantera/data/example_data/{yaml.name}", yaml.abspath,
Copy("$TARGET", "$SOURCE"))))
# Install Python samples
install(localenv.RecursiveInstall, "$inst_sampledir/python", "#samples/python")
setup_python_env(localenv)
logger.info(f"Using setuptools version {localenv['setuptools_version']}")
# Python module shouldn't explicitly link to Python library (added in other cases to
# support Python-based extensions), except when using MinGW
if localenv["toolchain"] != "mingw":
localenv["LIBS"] = [lib for lib in localenv["LIBS"] if not lib.startswith("python")]
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"])
directives = {"binding": True}
if env["coverage"]:
directives["linetrace"] = True
localenv.Append(CPPDEFINES={
"CYTHON_TRACE": 1,
# TODO: Remove after https://github.com/coveragepy/coveragepy/issues/1790 is resolved.
"CYTHON_USE_SYS_MONITORING": 0}
)
if env['debug']:
# Avoid needing to specify LD_LIBRARY_PATH or DYLD_LIBRARY_PATH when running the
# Python module from the build directory. Excluded for non-debug builds to avoid
# polluting RPATH with paths from the build environment.
if env["OS"] == "Darwin":
localenv.Append(
LINKFLAGS=env.subst(
f"${{RPATHPREFIX}}{localenv.Dir('#build/lib').abspath}${{RPATHSUFFIX}}"))
elif env["use_rpath_linkage"]:
localenv.Append(RPATH=localenv.Dir('#build/lib').abspath)
# Build the Python module
cython_obj = []
for pyxfile in multi_glob(localenv, "cantera", "pyx"):
cython_env = localenv
cython_output = f"cantera/{pyxfile.name.replace('.pyx', '.cpp')}"
if pyxfile.name == "_delegate_callbacks.pyx":
cython_output = [cython_output, "cantera/_delegate_callbacks.h"]
cythonized = cython_env.Command(
cython_output, pyxfile,
f'''${{python_cmd}} -c "import setuptools; import Cython.Build; Cython.Build.cythonize(r'${{SOURCE}}', compiler_directives={directives!r})"'''
)
for pxd in multi_glob(cython_env, "cantera", "pxd"):
cython_env.Depends(cythonized, pxd)
obj = cython_env.SharedObject(
f"#build/temp-py/{pyxfile.name.split('.')[0]}", cythonized[0])
cython_obj.append(obj)
# The aggregator module _cantera uses Cython pure-Python (.py) syntax, but unlike the
# standalone pure_py_modules below it is cythonized and compiled INTO the merged
# _cantera extension here: it hosts the CythonPackageMetaPathFinder and provides the
# PyInit functions for the remaining .pyx modules merged into this extension. Its .py
# source ships alongside the extension to serve tracebacks, IDEs, and type checkers.
_cantera_cpp = localenv.Command(
"cantera/_cantera.cpp", "cantera/_cantera.py",
f'''${{python_cmd}} -c "import setuptools; import Cython.Build; Cython.Build.cythonize(r'${{SOURCE}}', compiler_directives={directives!r})"'''
)
for pxd in multi_glob(localenv, "cantera", "pxd"):
localenv.Depends(_cantera_cpp, pxd)
cython_obj.append(localenv.SharedObject("#build/temp-py/_cantera", _cantera_cpp[0]))
copy_header = localenv.Command('#src/extensions/_delegate_callbacks.h',
'#build/python/cantera/_delegate_callbacks.h',
Copy('$TARGET', '$SOURCE'))
ext_manager = localenv.SharedObject("#src/extensions/PythonExtensionManager.cpp")
cython_obj.extend(ext_manager)
env.Depends(ext_manager, copy_header)
module_ext = localenv["py_module_ext"]
ext = localenv.LoadableModule(f"cantera/_cantera{module_ext}",
cython_obj, LIBPREFIX="", SHLIBSUFFIX=module_ext,
SHLIBPREFIX="", LIBSUFFIXES=[module_ext])
# Cython pure-Python-syntax modules built as their own extensions. Each ships its .py
# source alongside the compiled extension: the extension wins CPython import precedence,
# while the .py serves tracebacks, IDEs, and type checkers.
pure_py_modules = [
"constants", "delegator", "func1", "jacobians", "kinetics", "mixture", "_onedim",
"reaction", "reactionpath", "reactor", "solutionbase", "speciesthermo", "thermo",
"transport", "units", "_utils", "yamlwriter"
]
pure_py_ext = []
for _name in pure_py_modules:
if _name == "_utils":
# Define GIT_COMMIT only for _utils to avoid unnecessary recompilations
_pyenv = localenv.Clone()
_pyenv.Append(CPPDEFINES={'GIT_COMMIT': '\\"{0}\\"'.format(env['git_commit'])})
else:
_pyenv = localenv
_cythonized = _pyenv.Command(
f"cantera/{_name}.cpp", f"cantera/{_name}.py",
f'''${{python_cmd}} -c "import setuptools; import Cython.Build; '''
f'''Cython.Build.cythonize(r'${{SOURCE}}', compiler_directives={directives!r})"'''
)
for _pxd in multi_glob(_pyenv, "cantera", "pxd"):
_pyenv.Depends(_cythonized, _pxd)
_obj = _pyenv.SharedObject(f"#build/temp-py/{_name}", _cythonized[0])
_modext = _pyenv.LoadableModule(
f"cantera/{_name}{module_ext}", _obj,
LIBPREFIX="", SHLIBSUFFIX=module_ext, SHLIBPREFIX="",
LIBSUFFIXES=[module_ext])
# Each of these extensions links against the Cantera shared library
_pyenv.Depends(_modext, localenv["cantera_shlib"])
pure_py_ext.append(_modext)
build_cmd = ("$python_cmd_esc -m pip wheel -v --no-build-isolation --no-deps "
"--wheel-dir=build/python/dist build/python")
# We've already warned that this Python version might be unsupported, so disable
# pip's check for a correct version of Python.
if parse_version(localenv["py_version_short"]) >= env["python_max_version"]:
build_cmd += " --ignore-requires-python"
# Newer setuptools versions always lowercase the package name
spec1 = SpecifierSet(">=75.8.1")
spec2 = SpecifierSet("~=75.3.1") # change backported to this branch
if localenv["setuptools_version"] in spec1 or localenv["setuptools_version"] in spec2:
pkg_name = "cantera"
else:
pkg_name = "Cantera"
wheel_name = (pkg_name + "-${cantera_version}-cp${py_version_nodot}"
"-cp${py_version_nodot}-${py_plat}.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, pure_py_ext, dataFiles, setup_cfg, readme, license,
"setup.py", "pyproject.toml",
"cantera/test/README.txt", "cantera/examples/README.txt"])
if env['OS'] == 'Windows':
# On Windows, the cantera library directory is likely not to be on the path.
# However, Windows does search the directory containing a library (i.e. the
# Python extension module) for DLL dependencies.
dll = [f for f in localenv['cantera_shlib'] if f.name.endswith('.dll')][0]
copy_dll = localenv.Command(f'cantera/{dll.name}', dll, Copy("$TARGET", "$SOURCE"))
localenv.Depends(ext, copy_dll)
# If compiling with MinGW, there are some system libraries that also seem
# to need to be installed alongside the Python extension -- elsewhere on the path
# does not appear to work.
if env['toolchain'] == 'mingw':
mingw_dir = Path(which(env.subst('$CXX'))).parent
prefixes = ['libgcc', 'libstdc++', 'libwinpthread']
for lib in mingw_dir.glob('*.dll'):
if any(lib.name.startswith(prefix) for prefix in prefixes):
copy_lib = localenv.Command(f'cantera/{lib.name}', str(lib),
Copy('$TARGET', '$SOURCE'))
localenv.Depends(mod, copy_lib)
else:
localenv.Depends(ext, localenv['cantera_shlib'])
for f in (multi_glob(localenv, 'cantera', 'py', 'pyi') +
multi_glob(localenv, 'cantera/*', 'py', 'pyi')):
localenv.Depends(mod, f)
localenv.Depends(mod, 'cantera/py.typed')
units = localenv.UnitsInterfaceBuilder(
"cantera/with_units/solution.py",
"cantera/with_units/solution.py.in",
)
localenv.Depends(mod, units)
# Determine installation path and install the Python module
install_cmd = ["$python_cmd_esc", "-m", "pip", "install"]
user_install = False
python_prefix = 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"]:
# A specific location for the Cantera python module has been given
install_cmd.append(f"--prefix={localenv.subst('$python_prefix')}")
python_prefix = localenv.subst("$python_prefix")
elif not env["default_prefix"]:
install_cmd.append(f"--prefix={env['prefix']}")
python_prefix = env["prefix"]
# Check for existing Python module installation. Allow pip to remove an existing
# installation only if we're installing to the same location. Also disable
# uninstallation if we're installing to a staging directory.
if env["stage_dir"]:
install_cmd.append("--ignore-installed")
else:
info = get_command_output(localenv["python_cmd"], "-m", "pip", "show", "cantera",
ignore_errors=True)
if user_install:
test_prefix = Path(localenv["user_site_packages"]).parents[2]
elif python_prefix is None:
test_prefix = Path(localenv["site_packages"][0]).parents[2]
else:
test_prefix = Path(python_prefix)
match = re.search(r"Location: (.*)\n", info, re.MULTILINE)
existing_prefix = Path(match.group(1)).parents[2] if match else None
if existing_prefix and existing_prefix != test_prefix:
install_cmd.append("--ignore-installed")
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()}")
install_cmd.extend(("--no-build-isolation", "--no-deps", "-v", "--force-reinstall",
"$SOURCE"))
mod_inst = install(localenv.Command, "dummy", mod, " ".join(install_cmd))
env["install_python_action"] = mod_inst
install_locs = get_pip_install_location(localenv["python_cmd"], user_install,
python_prefix)
env["python_module_loc"] = Path(install_locs["platlib"]).as_posix()
env["ct_pyscriptdir"] = Path(install_locs["scripts"]).as_posix()