mirror of
https://github.com/Cantera/cantera.git
synced 2026-08-02 09:29:26 -05:00
256 lines
11 KiB
Python
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()
|