"""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()