From 67fe05ad18505d64c8ee5d20f11de81642906491 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Tue, 13 Jul 2021 21:55:11 -0400 Subject: [PATCH 01/14] [SCons] Add a target for files copied from ext --- ext/SConscript | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/ext/SConscript b/ext/SConscript index 309f1159dd3..f046b6a719c 100644 --- a/ext/SConscript +++ b/ext/SConscript @@ -42,6 +42,8 @@ for subdir, extensions, prepFunction in libs: objects = localenv.SharedObject(multi_glob(localenv, subdir, *extensions)) libraryTargets.extend(objects) +ext_copies = [] + if not env['system_fmt']: license_files.append(('fmtlib', 'fmt/LICENSE.rst')) localenv = prep_default(env) @@ -49,9 +51,11 @@ if not env['system_fmt']: libraryTargets.extend( localenv.SharedObject(multi_glob(localenv, 'fmt/src', 'cc'))) for name in ('format.h', 'ostream.h', 'printf.h', 'core.h', 'format-inl.h'): - build(copyenv.Command("#include/cantera/ext/fmt/" + name, - "#ext/fmt/include/fmt/" + name, - Copy('$TARGET', '$SOURCE'))) + ext_copies.extend( + copyenv.Command("#include/cantera/ext/fmt/" + name, + "#ext/fmt/include/fmt/" + name, + Copy("$TARGET", "$SOURCE")) + ) if env['system_sundials'] == 'n': localenv = prep_default(env) @@ -73,9 +77,11 @@ if env['system_sundials'] == 'n': for subdir in ('sundials', 'nvector', 'cvodes', 'ida', 'sunmatrix', 'sunlinsol', 'sunnonlinsol'): for header in multi_glob(env, 'sundials/include/'+subdir, 'h'): - build(copyenv.Command('#include/cantera/ext/%s/%s' % (subdir, header.name), - '#ext/sundials/include/%s/%s' % (subdir, header.name), - Copy('$TARGET', '$SOURCE'))) + ext_copies.extend( + copyenv.Command(f"#include/cantera/ext/{subdir}/{header.name}", + f"#ext/sundials/include/{subdir}/{header.name}", + Copy("$TARGET", "$SOURCE")) + ) # Compile Sundials source files. Skip files related to the Sundials Fortran # interface, which start with 'fsun'. @@ -98,9 +104,13 @@ if not env['system_yamlcpp']: # Copy header files into common include directory for subdir in ('', 'contrib', 'node', 'node/detail'): for header in multi_glob(env, 'yaml-cpp/include/yaml-cpp/'+subdir, 'h'): - h = build(localenv.Command('#include/cantera/ext/yaml-cpp/{}/{}'.format(subdir, header.name), - '#ext/yaml-cpp/include/yaml-cpp/{}/{}'.format(subdir, header.name), - Copy('$TARGET', '$SOURCE'))) + ext_copies.extend( + localenv.Command( + f"#include/cantera/ext/yaml-cpp/{subdir}/{header.name}", + f"#ext/yaml-cpp/include/yaml-cpp/{subdir}/{header.name}", + Copy("$TARGET", "$SOURCE") + ) + ) # Compile yaml-cpp source files for subdir in ('', 'contrib'): @@ -113,6 +123,7 @@ if not env['system_eigen']: h = build(copyenv.Command('#include/cantera/ext/Eigen', '#ext/eigen/Eigen', Copy('$TARGET', '$SOURCE'))) copyenv.Depends(copyenv['config_h_target'], h) + ext_copies.extend(h) # Google Test: Used internally for Cantera unit tests. if env['googletest'] == 'submodule': @@ -123,6 +134,8 @@ if env['googletest'] == 'submodule': gmock = build(localenv.Library('../lib/gmock', source=['googletest/googlemock/src/gmock-all.cc'])) +env["ext_include_copies_target"] = build(ext_copies) + # Create license file containing licenses for Cantera and all included packages def generate_license(target, source, env): stars = '*'*50 + '\n' + '*'*50 + '\n' From c3be9043ae00c8a27bb78b8e620bf0f342a4cfe2 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 12:35:04 -0400 Subject: [PATCH 02/14] [SCons] Use pathlib to write combined License file Use a dictionary to collect the license file paths and names. This data structure is more natural for this mapping of packages to files. It also simplifies the loop to collect the files. --- ext/SConscript | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/ext/SConscript b/ext/SConscript index f046b6a719c..0358cc072eb 100644 --- a/ext/SConscript +++ b/ext/SConscript @@ -1,11 +1,12 @@ +from pathlib import Path from buildutils import * Import('env', 'build', 'install', 'libraryTargets') localenv = env.Clone() copyenv = localenv.Clone() # no CPPPATH addition, to avoid circular dependencies -license_files = [('Cantera', '#License.txt'), - ('Libexecstream', 'libexecstream/doc/license.txt')] +license_files = {"Cantera": File("#License.txt"), + "Libexecstream": File("#ext/libexecstream/doc/license.txt")} def prep_default(env): localenv = env.Clone() @@ -45,7 +46,7 @@ for subdir, extensions, prepFunction in libs: ext_copies = [] if not env['system_fmt']: - license_files.append(('fmtlib', 'fmt/LICENSE.rst')) + license_files["fmtlib"] = File("#ext/fmt/LICENSE.rst") localenv = prep_default(env) localenv.Prepend(CPPPATH=Dir('#ext/fmt/include')) libraryTargets.extend( @@ -61,7 +62,7 @@ if env['system_sundials'] == 'n': localenv = prep_default(env) localenv.Prepend(CPPPATH=[Dir('#include/cantera/ext'), Dir('#ext/sundials/src/sundials')]) - license_files.append(('Sundials', 'sundials/LICENSE')) + license_files["Sundials"] = File("#ext/sundials/LICENSE") # Generate sundials_config.h sundials_configh = {} @@ -99,7 +100,7 @@ if env['system_sundials'] == 'n': if not env['system_yamlcpp']: localenv = prep_default(env) localenv.Prepend(CPPPATH=Dir('#include/cantera/ext')) - license_files.append(('YAML-CPP', 'yaml-cpp/LICENSE')) + license_files["YAML-CPP"] = File("#ext/yaml-cpp/LICENSE") # Copy header files into common include directory for subdir in ('', 'contrib', 'node', 'node/detail'): @@ -119,7 +120,7 @@ if not env['system_yamlcpp']: if not env['system_eigen']: - license_files.append(('Eigen', 'eigen/COPYING.MPL2')) + license_files["Eigen"] = File("#ext/eigen/COPYING.MPL2") h = build(copyenv.Command('#include/cantera/ext/Eigen', '#ext/eigen/Eigen', Copy('$TARGET', '$SOURCE'))) copyenv.Depends(copyenv['config_h_target'], h) @@ -136,28 +137,33 @@ if env['googletest'] == 'submodule': env["ext_include_copies_target"] = build(ext_copies) -# Create license file containing licenses for Cantera and all included packages + def generate_license(target, source, env): - stars = '*'*50 + '\n' + '*'*50 + '\n' - tpl = stars + 'The following license applies to {}\n' + stars + '\n{}\n' + target = Path(target[0].abspath) + stars = "*" * 50 + "\n" + "*" * 50 + "\n" + tpl = stars + "The following license applies to {}\n" + stars + "\n{}\n" license = [] - for (package,_),filename in zip(license_files, source): - license.append(tpl.format(package, open(filename.path).read().strip())) + for package, license_file in env["license_files"].items(): + license_file = Path(license_file.abspath) + license.append(tpl.format(package, license_file.read_text().strip())) + license = "\n".join(license) + if target.suffix == ".rtf": + license = license.replace("\\", "\\\\").replace("{", "\\{").replace("}", "\\}") + license = license.replace("\n", " \\par\n") + license = (r"{\rtf1\ansi{\fonttbl\f0\fswiss Arial;}\f0\pard\fs16 " + + license + "}") - license = '\n'.join(license) - if target[0].path.endswith('.rtf'): - license = license.replace('\\', '\\\\').replace('{', '\\{').replace('}', '\\}') - license = license.replace('\n', ' \\par\n') - license = r'{\rtf1\ansi{\fonttbl\f0\fswiss Arial;}\f0\pard\fs16 ' + license + '}' + target.write_text(license) - open(target[0].path, 'w').write(license) -license = build(localenv.Command('LICENSE.txt', [x[1] for x in license_files], +localenv["license_files"] = license_files +license = build(localenv.Command("LICENSE.txt", license_files.values(), generate_license)) +env["license_target"] = license install('$inst_docdir', license) if env['OS'] == 'Windows': # RTF version is required for Windows installer - build(localenv.Command('LICENSE.rtf', [x[1] for x in license_files], + build(localenv.Command("LICENSE.rtf", license_files.values(), generate_license)) From cdccd7a4bd598a6ab4a8349ba36dd9cbefeb7e75 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 3 Feb 2021 10:15:57 -0500 Subject: [PATCH 03/14] [SCons/Python] Create the source distribution Add a new option for the Python package to build a source distribution. It is added as a new interface to simplify copying files. All of the Cantera plus external library source code is copied into the interface folder for building. Actually building the source distribution is handled by the 'build' package, which becomes a new dependency to build the sdist. 'build' handles installing all the build-time dependencies in an isolated environment using specifications in pyproject.toml. --- SConstruct | 21 +++- interfaces/python_sdist/MANIFEST.in | 11 ++ interfaces/python_sdist/SConscript | 147 ++++++++++++++++++++++++ interfaces/python_sdist/pyproject.toml | 3 + interfaces/python_sdist/setup.cfg.in | 69 ++++++++++++ interfaces/python_sdist/setup.py | 148 +++++++++++++++++++++++++ 6 files changed, 393 insertions(+), 6 deletions(-) create mode 100644 interfaces/python_sdist/MANIFEST.in create mode 100644 interfaces/python_sdist/SConscript create mode 100644 interfaces/python_sdist/pyproject.toml create mode 100644 interfaces/python_sdist/setup.cfg.in create mode 100644 interfaces/python_sdist/setup.py diff --git a/SConstruct b/SConstruct index 46b26d2bd2e..5354852fb86 100644 --- a/SConstruct +++ b/SConstruct @@ -88,7 +88,8 @@ if os.name not in ["nt", "posix"]: sys.exit(1) valid_commands = ("build", "clean", "install", "uninstall", - "help", "msi", "samples", "sphinx", "doxygen", "dump") + "help", "msi", "samples", "sphinx", "doxygen", "dump", + "sdist") for command in COMMAND_LINE_TARGETS: if command not in valid_commands and not command.startswith('test'): @@ -239,6 +240,10 @@ config_options = [ Cython) are installed. Note: 'y' is a synonym for 'full' and 'n' is a synonym for 'none'.""", "default", ("full", "minimal", "none", "n", "y", "default")), + BoolOption( + "python_sdist", + """Setting this option to True builds the Python sdist.""", + False), PathOption( "python_cmd", """Cantera needs to know where to find the Python interpreter. If @@ -817,6 +822,8 @@ if 'doxygen' in COMMAND_LINE_TARGETS: env['doxygen_docs'] = True if 'sphinx' in COMMAND_LINE_TARGETS: env['sphinx_docs'] = True +if "sdist" in COMMAND_LINE_TARGETS: + env["python_sdist"] = True for arg in ARGUMENTS: if arg not in config: @@ -1349,7 +1356,7 @@ if env['VERBOSE']: env['python_cmd_esc'] = quoted(env['python_cmd']) # Python Package Settings -python_min_version = parse_version('3.5') +python_min_version = parse_version("3.6") # The string is used to set python_requires in setup.py.in env['py_min_ver_str'] = str(python_min_version) # Note: cython_min_version is redefined below if the Python version is 3.8 or higher @@ -1868,6 +1875,10 @@ if env['matlab_toolbox'] == 'y': if env['doxygen_docs'] or env['sphinx_docs']: SConscript('doc/SConscript') +if env["python_sdist"]: + VariantDir("build/python_sdist", "interfaces/python_sdist", duplicate=1) + SConscript("interfaces/python_sdist/SConscript", variant_dir="build/python_sdist") + # Sample programs (also used from test_problems/SConscript) VariantDir('build/samples', 'samples', duplicate=0) sampledir_excludes = ['\\.o$', '^~$', '\\.in', 'SConscript'] @@ -1896,12 +1907,10 @@ def postBuildMessage(target, source, env): print("- To run the test suite, type 'scons test'.") print("- To list available tests, type 'scons test-help'.") if env['googletest'] == 'none': - print(" WARNING: You set the 'googletest' to 'none' and all it's tests will be skipped.") + print(" WARNING: You set the 'googletest' to 'none' and all its tests will be skipped.") + print("- To install, type 'scons install'.") if os.name == 'nt': - print("- To install, type 'scons install'.") print("- To create a Windows MSI installer, type 'scons msi'.") - else: - print("- To install, type 'scons install'.") print("*******************************************************") finish_build = env.Command('finish_build', [], postBuildMessage) diff --git a/interfaces/python_sdist/MANIFEST.in b/interfaces/python_sdist/MANIFEST.in new file mode 100644 index 00000000000..f95bf9da68c --- /dev/null +++ b/interfaces/python_sdist/MANIFEST.in @@ -0,0 +1,11 @@ +include cantera/_cantera.cpp +include cantera/_cantera.h +recursive-include cantera *.pyx +include sundials_config.h.in +graft include +recursive-include ext *.h +recursive-include src *.h +recursive-include ext/libexecstream/posix *.cpp +recursive-include ext/libexecstream/win *.cpp +exclude include/cantera/ext/sundials/sundials_config.h +exclude include/cantera/base/config.h.in diff --git a/interfaces/python_sdist/SConscript b/interfaces/python_sdist/SConscript new file mode 100644 index 00000000000..f45a789024d --- /dev/null +++ b/interfaces/python_sdist/SConscript @@ -0,0 +1,147 @@ +"""SDist of the Python Module""" +from pathlib import Path +import re +import shutil +from textwrap import dedent + +from build import ProjectBuilder +from build.env import IsolatedEnvBuilder + +from buildutils import logger + +Import("env") + +localenv = env.Clone() + +sdist_targets = [] + + +def sdist(targets): + sdist_targets.extend(targets) + return targets + + +def copy_ext_src(target, source, env): + target_ext = Path(target[0].abspath) + source_ext = Path(source[0].abspath) + if target_ext.is_dir(): + shutil.rmtree(target_ext) + + # fmt library + FMT_ROOT = target_ext / "fmt" + FMT_ROOT.mkdir(parents=True) # Needed to be able to use copy2 + for cc_file in (source_ext / "fmt" / "src").glob("*.cc"): + shutil.copy2(cc_file, FMT_ROOT) + shutil.copytree(source_ext / "fmt" / "include" / "fmt", FMT_ROOT / "fmt") + + # libexecstream + EXECSTREAM_ROOT = target_ext / "libexecstream" + EXECSTREAM_ROOT.mkdir(parents=True) # Needed to be able to use copy2 + shutil.copy2(source_ext / "libexecstream" / "exec-stream.cpp", EXECSTREAM_ROOT) + shutil.copy2(source_ext / "libexecstream" / "exec-stream.h", EXECSTREAM_ROOT) + shutil.copytree(source_ext / "libexecstream" / "win", EXECSTREAM_ROOT / "win") + shutil.copytree(source_ext / "libexecstream" / "posix", EXECSTREAM_ROOT / "posix") + + # yaml-cpp library + YAML_ROOT = target_ext / "yaml-cpp" + shutil.copytree(source_ext / "yaml-cpp" / "src", YAML_ROOT) + shutil.copytree(source_ext / "yaml-cpp" / "include" / "yaml-cpp", + YAML_ROOT / "yaml-cpp") + + # SUNDIALS library + SUNDIALS_ROOT = target_ext / "sundials" + subdirs = ["sundials", "nvector/serial", "cvodes", "ida", "sunmatrix/band", + "sunmatrix/dense", "sunmatrix/sparse", "sunlinsol/dense", + "sunlinsol/band", "sunlinsol/spgmr", "sunnonlinsol/newton"] + ignores = shutil.ignore_patterns("fsun*", "CMake*", "fmod", "fcmix") + for subdir in subdirs: + shutil.copytree( + source_ext / "sundials" / "src" / subdir, + SUNDIALS_ROOT / subdir, + ignore=ignores, + ) + + +def replace_git_hash(target, source, env): + # Avoid having to set a C preprocessor define at compile time, since + # the git commit is unknown from the sdist + target = Path(target[0].abspath) + source = Path(source[0].abspath) + git_commit_replaced = re.sub("#ifdef GIT_COMMIT.*?#endif", + f""" return "{env['git_commit']}";""", + source.read_text(), + flags=re.DOTALL) + target.write_text(git_commit_replaced) + + +# Use RecursiveInstall to be able to exclude files and folders. +sdist(localenv.RecursiveInstall( + "src", + "#src", + exclude=["fortran", "matlab", r"global\.cpp", "SCons.*"], +)) + +sdist(localenv.Command("src/base/global.cpp", "#src/base/global.cpp", + replace_git_hash)) + +include_target = sdist(localenv.Command("include", "#include", + Copy("$TARGET", "$SOURCE"))) +localenv.Depends(include_target, env["config_h_target"]) +localenv.Depends(include_target, env["ext_include_copies_target"]) + +sdist(localenv.Command("ext", "#ext", copy_ext_src)) + +# Use RecursiveInstall to make sure that files are not overwritten during the copy. +# A normal Copy Action would fail because of the existing directories. +sdist(localenv.RecursiveInstall("cantera", + "#interfaces/cython/cantera", + exclude=["__pycache__"])) +sdist(localenv.RecursiveInstall("cantera/data", + "#data", exclude=[r"\.xml", r"\.cti"])) +sdist(localenv.RecursiveInstall("cantera/test/data", + "#test/data")) + +# Copy the minimal Sundials configuration template into the sdist so that +# it can be filled in at compile time on the user's machine +sdist(localenv.Command("sundials_config.h.in", "#ext/sundials_config.h.in", + Copy("$TARGET", "$SOURCE"))) + +license = sdist(localenv.Command("LICENSE.txt", "#build/ext/LICENSE.txt", + Copy("$TARGET", "$SOURCE"))) +localenv.Depends(license, localenv["license_target"]) + +sdist(localenv.SubstFile("setup.cfg", "setup.cfg.in")) +sdist(localenv.Command("README.rst", "#README.rst", Copy("$TARGET", "$SOURCE"))) + + +def build_sdist(target, source, env): + build_dir = Path(source[0].abspath).parent + builder = ProjectBuilder(str(build_dir)) + with IsolatedEnvBuilder() as build_env: + builder.python_executable = build_env.executable + builder.scripts_dir = build_env.scripts_dir + # first install the build dependencies + build_env.install(builder.build_system_requires) + # then get the extra required dependencies from the backend + build_env.install(builder.get_requires_for_build("sdist")) + builder.build("sdist", str(build_dir / "dist"), {}) + + +def finish_sdist_message(target, source, env): + sdist = Path(source[0].path).name + message = dedent(f""" + ******************************************************* + Python sdist '{sdist}' created successfully. + The sdist file is in the 'build/python_sdist/dist' + directory. + ******************************************************* + """) + logger.info(message, print_level=False) + + +sdist_target = f"dist/Cantera-{env['cantera_version']}.tar.gz" +sdist_sources = ("setup.py", "pyproject.toml", "MANIFEST.in") +built_sdist = localenv.Command(sdist_target, sdist_sources, build_sdist) +finish_sdist = localenv.Command("finish_sdist", sdist_target, finish_sdist_message) +localenv.Depends(built_sdist, sdist_targets) +env.Alias("sdist", finish_sdist) diff --git a/interfaces/python_sdist/pyproject.toml b/interfaces/python_sdist/pyproject.toml new file mode 100644 index 00000000000..a272bb9561d --- /dev/null +++ b/interfaces/python_sdist/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=43.0.0", "wheel", "oldest-supported-numpy", "Cython>=0.29.12"] +build-backend = "setuptools.build_meta" diff --git a/interfaces/python_sdist/setup.cfg.in b/interfaces/python_sdist/setup.cfg.in new file mode 100644 index 00000000000..488a297da26 --- /dev/null +++ b/interfaces/python_sdist/setup.cfg.in @@ -0,0 +1,69 @@ +[metadata] +name = Cantera +version = @cantera_version@ +description = Cantera is an open-source suite of tools for problems involving chemical kinetics, thermodynamics, and transport processes. +long_description = file: README.rst +long_description_content_type = text/x-rst +license_files = LICENSE.txt +url = https://cantera.org +author = Cantera Developers +author_email = developers@cantera.org +keywords = chemistry physics +license = BSD 3-Clause License +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Education + Intended Audience :: Science/Research + License :: OSI Approved :: BSD License + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX :: Linux + Programming Language :: C + Programming Language :: C++ + Programming Language :: Cython + Programming Language :: Fortran + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + Topic :: Scientific/Engineering :: Chemistry + Topic :: Scientific/Engineering :: Physics +project_urls = + Documentation = https://cantera.org/documentation + Funding = https://numfocus.org/donate-to-cantera + Source = https://github.com/Cantera/cantera + Tracker = https://github.com/Cantera/cantera/issues + +[options] +zip_safe = False +include_package_data = True +install_requires = + numpy >= 1.12.0 + ruamel.yaml >= 0.15.34 +python_requires = >=@py_min_ver_str@ +packages = + cantera + cantera.data + cantera.test + cantera.test.data + cantera.examples + +[options.package_data] +cantera.data = *.yaml, */*.yaml +cantera.test.data = *.*, */*.* +cantera.examples = */*.* +cantera = *.pxd + +[options.extras_require] +hdf5 = h5py +pandas = pandas + +[options.entry_points] +console_scripts = + ck2cti = cantera.ck2cti:script_entry_point + ctml_writer = cantera.ctml_writer:main + ck2yaml = cantera.ck2yaml:script_entry_point + cti2yaml = cantera.cti2yaml:main + ctml2yaml = cantera.ctml2yaml:main diff --git a/interfaces/python_sdist/setup.py b/interfaces/python_sdist/setup.py new file mode 100644 index 00000000000..4bd16ab71e1 --- /dev/null +++ b/interfaces/python_sdist/setup.py @@ -0,0 +1,148 @@ +import sys +import os +from setuptools import setup, Extension +from setuptools.command.install import install +from setuptools.command.develop import develop +from pathlib import Path +import numpy +import shutil + +HERE = Path(__file__).parent +CT_SRC = HERE / "src" +EXT_SRC = HERE / "ext" +CT_INCLUDE = HERE / "include" +BOOST_INCLUDE = None +FORCE_CYTHON_COMPILE = False + +CYTHON_BUILT_FILES = [HERE / "cantera" / f"_cantera.{ext}" for ext in ("cpp", "h")] + + +class CanteraOptionsMixin: + """Custom options for the install and develop commands. + + Modeled after https://stackoverflow.com/a/53833930 + """ + user_options = [ + ("force-cython-compile", None, "Force compilation of .pyx files via Cython"), + ("boost-include", None, "Location of the Boost header files."), + ] + + def initialize_options(self): + super().initialize_options() + self.force_cython_compile = False + self.boost_include = None + + def finalize_options(self): + if self.boost_include is not None: + if not Path(self.boost_include).is_dir(): + raise TypeError(f"The path {self.boost_include!r} is not a directory.") + super().finalize_options() + + def run(self): + global BOOST_INCLUDE, FORCE_CYTHON_COMPILE + BOOST_INCLUDE = self.boost_include + FORCE_CYTHON_COMPILE = self.force_cython_compile + super().run() + + +class InstallCommand(CanteraOptionsMixin, install): + user_options = (getattr(install, "user_options", []) + + CanteraOptionsMixin.user_options) + + +class DevelopCommand(CanteraOptionsMixin, develop): + user_options = (getattr(develop, "user_options", []) + + CanteraOptionsMixin.user_options) + + +if ( + not all(p.exists() for p in CYTHON_BUILT_FILES) + or "sdist" in sys.argv + or FORCE_CYTHON_COMPILE + or os.environ.get("FORCE_CYTHON_COMPILE", False) +): + from Cython.Build import cythonize + CYTHON_EXT = ".pyx" + for p in CYTHON_BUILT_FILES: + if p.exists(): + p.unlink() +else: + CYTHON_EXT = ".cpp" + + def cythonize(extensions): + """Define a no-op for when we're not using Cython.""" + return extensions + +source_files = ["cantera/_cantera" + CYTHON_EXT] +source_files += list(map(str, CT_SRC.glob("**/*.cpp"))) +sundials_sources = list(map(str, EXT_SRC.glob("sundials/**/*.c"))) +yaml_cpp_sources = list(map(str, EXT_SRC.glob("yaml-cpp/**/*.cpp"))) +fmt_sources = list(map(str, EXT_SRC.glob("fmt/*.cc"))) +libexecstream_sources = [str(EXT_SRC / "libexecstream" / "exec-stream.cpp")] + +include_dirs = [ + str(CT_INCLUDE), + str(CT_INCLUDE / "cantera" / "ext"), + str(CT_SRC), + numpy.get_include() +] + +if "BOOST_INCLUDE" in os.environ: + include_dirs.append(os.environ["BOOST_INCLUDE"]) +elif BOOST_INCLUDE is not None: + include_dirs.append(BOOST_INCLUDE) + +if sys.platform != "win32": + extra_compile_flags = ["-std=c++11"] + sundials_configh = { + "SUNDIALS_USE_GENERIC_MATH": "#define SUNDIALS_USE_GENERIC_MATH 1", + "SUNDIALS_BLAS_LAPACK": "/* #undef SUNDIALS_BLAS_LAPACK */" + } + sundials_cflags = ["-w"] + sundials_macros = [] +else: + extra_compile_flags = [] + sundials_macros = [("_CRT_SECURE_NO_WARNINGS", None)] + sundials_configh = { + "SUNDIALS_USE_GENERIC_MATH": "/* #undef SUNDIALS_USE_GENERIC_MATH */", + "SUNDIALS_BLAS_LAPACK": "/* #undef SUNDIALS_BLAS_LAPACK */" + } + sundials_cflags = [] + +config_h_in = (HERE / "sundials_config.h.in").read_text() +config_h = HERE / "sundials_config.h" +config_h.write_text(config_h_in.format_map(sundials_configh)) +shutil.copy2(config_h, EXT_SRC / "sundials" / "sundials") +shutil.copy2(config_h, CT_INCLUDE / "cantera" / "ext" / "sundials") + +extensions = cythonize([ + Extension( + "cantera._cantera", + source_files, + include_dirs=include_dirs, + extra_compile_args=extra_compile_flags, + language="c++", + ), +]) + + +def lib_def(sources, cflags, include_dirs, macros): + """Convenience factory to create the dictionary for a Setuptools library build.""" + return dict(sources=sources, cflags=cflags, include_dirs=include_dirs, + macros=macros) + + +sundials_inc_dir = include_dirs + [str(EXT_SRC / "sundials" / "sundials")] +libraries = [ + ("sundials", lib_def(sundials_sources, sundials_cflags, sundials_inc_dir, + sundials_macros)), + ("yaml-cpp", lib_def(yaml_cpp_sources, extra_compile_flags, include_dirs, [])), + ("fmtlib", lib_def(fmt_sources, extra_compile_flags, include_dirs, [])), + ("libexecstream", {"sources": libexecstream_sources, "include_dirs": include_dirs}) +] + +setup( + ext_modules=extensions, + libraries=libraries, + cmdclass={"install": InstallCommand, "develop": DevelopCommand}, +) From 46876b13d3c276ee2572e939ff979189b8e4c8a3 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Tue, 13 Jul 2021 22:01:48 -0400 Subject: [PATCH 04/14] [SCons] Build sundials_config and copy separately This is the same mechanism as employed for config.h in the main SConstruct. It's needed here to avoid repeatedly copying the include directory into the sdist, which fails due to the existing directory. --- ext/SConscript | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ext/SConscript b/ext/SConscript index 0358cc072eb..e66f7e3c17f 100644 --- a/ext/SConscript +++ b/ext/SConscript @@ -8,6 +8,7 @@ copyenv = localenv.Clone() # no CPPPATH addition, to avoid circular dependencies license_files = {"Cantera": File("#License.txt"), "Libexecstream": File("#ext/libexecstream/doc/license.txt")} + def prep_default(env): localenv = env.Clone() @@ -70,9 +71,17 @@ if env['system_sundials'] == 'n': sundials_configh['SUNDIALS_USE_GENERIC_MATH'] = 1 if env['use_lapack']: sundials_configh['SUNDIALS_BLAS_LAPACK'] = 1 - localenv.AlwaysBuild(env.Command('#include/cantera/ext/sundials/sundials_config.h', - 'sundials_config.h.in', - ConfigBuilder(sundials_configh))) + sundials_configh_build = env.Command("#build/ext/sundials_config.h.build", + "sundials_config.h.in", + ConfigBuilder(sundials_configh)) + # This separate copy operation, which SCons will skip if sundials_config.h.build is + # unmodified, prevents unnecessary re-copies of files in the #include directory + localenv.AlwaysBuild(sundials_configh_build) + ext_copies.extend( + localenv.Command("#include/cantera/ext/sundials/sundials_config.h", + "#build/ext/sundials_config.h.build", + Copy("$TARGET", "$SOURCE")) + ) # Copy sundials header files into common include directory for subdir in ('sundials', 'nvector', 'cvodes', 'ida', 'sunmatrix', From 75d586ec5152c912ff15bd2386bb1eb4e7e7856f Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 13:56:15 -0400 Subject: [PATCH 05/14] [CI] Build Python wheels using GitHub Actions --- .github/workflows/python-package.yml | 233 +++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 00000000000..73aa2968fcb --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,233 @@ +name: Build Python Package + +on: + push: + branches: + - add-pypi-package + +jobs: + sdist: + name: Build the sdist + runs-on: ubuntu-20.04 + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt install libboost-dev + - uses: actions/checkout@v2 + name: Checkout the repository + with: + submodules: recursive + - name: Set Up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: '3.8' + - name: Update pip + run: python3 -m pip install -U pip setuptools + - name: Install dependencies + run: python3 -m pip install scons numpy cython build + - name: Build the interface + run: python3 `which scons` build f90_interface=n debug=n googletest=none python_package=sdist use_pch=n + - name: Build the sdist + run: python3 -m build -s + working-directory: interfaces/python_sdist + - name: Archive the built sdist + uses: actions/upload-artifact@v2 + with: + path: ./interfaces/python_sdist/dist/*.tar.gz + name: sdist + if-no-files-found: error + + manylinux-wheel: + name: Build ${{ matrix.arch }} Manylinux Wheels for py${{ matrix.py }} + runs-on: ubuntu-20.04 + needs: ["sdist"] + strategy: + matrix: + py: ["36", "37", "38", "39"] + arch: ["x86_64", "i686", "aarch64", "ppc64le", "s390x"] + fail-fast: true + env: + BOOST_INCLUDE: ${{ github.workspace }}/include + BOOST_URL: https://pilotfiber.dl.sourceforge.net/project/boost/boost/1.75.0/boost_1_75_0.7z + steps: + - name: Download pre-built sdist + uses: actions/download-artifact@v2 + with: + name: sdist + - name: Extract the sdist tarball + run: tar -xvf *.tar.gz --strip-components=1 + - name: Restore Boost cache + uses: actions/cache@v2 + id: cache-boost + with: + path: ${{env.BOOST_INCLUDE}} + key: boost-${{env.BOOST_URL}} + - name: Install Boost Headers + if: steps.cache-boost.outputs.cache-hit != 'true' + run: | + mkdir -p $BOOST_INCLUDE + curl --progress-bar --location --output $BOOST_INCLUDE/download.7z $BOOST_URL + 7z -o$BOOST_INCLUDE x $BOOST_INCLUDE/download.7z -y -bd boost_1_75_0/boost + mv $BOOST_INCLUDE/boost_1_75_0/boost $BOOST_INCLUDE/boost + rm $BOOST_INCLUDE/download.7z + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + - name: Build wheels + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_BUILD: cp${{ matrix.py }}-* + CIBW_ARCHS: ${{ matrix.arch }} + + - name: Archive the built wheels + uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + name: wheels + + windows-wheel: + name: Build ${{ matrix.arch }} Windows Wheels for py${{ matrix.py }} + runs-on: windows-2019 + needs: ["sdist"] + strategy: + matrix: + py: ["36", "37", "38", "39"] + arch: ["AMD64", "x86"] + fail-fast: true + env: + BOOST_ROOT: ${{ github.workspace }}/3rdparty/boost + BOOST_URL: https://pilotfiber.dl.sourceforge.net/project/boost/boost/1.75.0/boost_1_75_0.7z + steps: + - name: Download pre-built sdist + uses: actions/download-artifact@v2 + with: + name: sdist + - name: Extract the sdist tarball + run: tar -xvf *.tar.gz --strip-components=1 + shell: bash + - name: Restore Boost cache + uses: actions/cache@v2 + id: cache-boost + with: + path: ${{env.BOOST_ROOT}} + key: boost-${{env.BOOST_URL}} + - name: Install Boost Headers + if: steps.cache-boost.outputs.cache-hit != 'true' + run: | + BOOST_ROOT=$(echo $BOOST_ROOT | sed 's/\\/\//g') + mkdir -p $BOOST_ROOT + curl --progress-bar --location --output $BOOST_ROOT/download.7z $BOOST_URL + 7z -o$BOOST_ROOT x $BOOST_ROOT/download.7z -y -bd boost_1_75_0/boost + mv $BOOST_ROOT/boost_1_75_0/boost $BOOST_ROOT/boost + rm $BOOST_ROOT/download.7z + shell: bash + - name: Build wheels + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_ENVIRONMENT: "BOOST_INCLUDE=${BOOST_ROOT}" + CIBW_ARCHS: ${{ matrix.arch }} + CIBW_BUILD: cp${{ matrix.py }}-* + CIBW_SKIP: cp35-* + - name: Archive the built wheels + uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + name: wheels + + macos-intel-wheel: + name: Build Intel macOS Wheels for py${{ matrix.py }} + runs-on: macos-10.15 + needs: ["sdist"] + env: + MACOSX_DEPLOYMENT_TARGET: 10.9 + strategy: + matrix: + py: ["36", "37", "38", "39"] + fail-fast: true + steps: + - name: Download pre-built sdist + uses: actions/download-artifact@v2 + with: + name: sdist + - name: Extract the sdist tarball + run: tar -xvf *.tar.gz --strip-components=1 + - name: Install Brew dependencies + run: brew install boost + - name: Build wheels + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_BUILD: cp${{ matrix.py }}-* + CIBW_SKIP: cp35-* + CIBW_ARCHS: "x86_64" + + - name: Archive the built wheels + uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + name: wheels + + macos-arm-wheel: + name: Build arm64 macOS Wheels for py${{ matrix.py }} + runs-on: macos-10.15 + needs: ["sdist"] + strategy: + matrix: + py: ["38", "39"] + fail-fast: true + env: + SDKROOT: "macosx11.0" + _PYTHON_HOST_PLATFORM: "macosx-11.0-arm64" + ARCHFLAGS: "-arch arm64" + MACOSX_DEPLOYMENT_TARGET: "10.9" + DEVELOPER_DIR: "/Applications/Xcode_12.2.app/Contents/Developer" + steps: + - name: Download pre-built sdist + uses: actions/download-artifact@v2 + with: + name: sdist + - name: Extract the sdist tarball + run: tar -xvf *.tar.gz --strip-components=1 + - name: Install Brew dependencies + run: brew install boost + - name: Build wheels + uses: pypa/cibuildwheel@v1.12.0 + env: + CIBW_ENVIRONMENT: 'BOOST_INCLUDE="$(brew --prefix)/include"' + CIBW_ARCHS: "arm64" + CIBW_BUILD: cp${{ matrix.py }}-* + - name: Archive the built wheels + uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + name: wheels + + publish-files-to-pypi: + name: Publish distribution files to PyPI + runs-on: ubuntu-20.04 + needs: + - "sdist" + - "manylinux-wheel" + - "windows-wheel" + - "macos-intel-wheel" + - "macos-arm-wheel" + steps: + - name: Download pre-built wheels + uses: actions/download-artifact@v2 + with: + path: dist/ + name: wheels + - name: Download pre-build sdist + uses: actions/download-artifact@v2 + with: + path: dist/ + name: sdist + - name: pypi-publish + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + # Ignore existing packages as long as we're uploading to TestPyPI + skip_existing: true From 479e2ebee2d59a46ae0a5957bd1f953776cabf87 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 14:35:55 -0400 Subject: [PATCH 06/14] Deprecate global canteraRoot function The function is not used anywhere within Cantera. When this function is removed, the CANTERA_ROOT define in config.h can be removed as well. --- include/cantera/base/global.h | 1 + src/base/global.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/include/cantera/base/global.h b/include/cantera/base/global.h index fcc76a370f1..908efabd5d9 100644 --- a/include/cantera/base/global.h +++ b/include/cantera/base/global.h @@ -113,6 +113,7 @@ std::string gitCommit(); * @returns a string containing the name of the base directory where %Cantera is * installed. If the environmental variable CANTERA_ROOT is defined, this * function will return its value, preferentially. + * @deprecated Unused within Cantera. To be removed after Cantera 2.6 * * @ingroup inputfiles */ diff --git a/src/base/global.cpp b/src/base/global.cpp index 14f8e2d25a8..d0a0377640f 100644 --- a/src/base/global.cpp +++ b/src/base/global.cpp @@ -169,6 +169,8 @@ doublereal actEnergyToSI(const std::string& unit) string canteraRoot() { + warn_deprecated("canteraRoot", + "Unused in Cantera. To be removed after Cantera 2.6"); char* ctroot = getenv("CANTERA_ROOT"); if (ctroot != 0) { return string(ctroot); From 21214c42d426c011689adf00e19d64e52af0fd37 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 15:07:54 -0400 Subject: [PATCH 07/14] [Cython] Import sysconfig directly The sysconfig module is available since Python 3.2. Distutils is deprecated and slated for removal from Python in the next few years. Likewise, the "SO" config variable has been deprecated for some time and was slated for removal in Python 3.8, although that seems not to have happened. --- interfaces/cython/SConscript | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/SConscript b/interfaces/cython/SConscript index c55debb2fb6..1477d32b2dd 100644 --- a/interfaces/cython/SConscript +++ b/interfaces/cython/SConscript @@ -32,9 +32,9 @@ testFiles = localenv.RecursiveInstall('#interfaces/cython/cantera/test/data', build(testFiles) # Get information needed to build the Python module -script = '\n'.join(("from distutils.sysconfig import *", +script = '\n'.join(("from sysconfig import *", "import numpy", - "print(get_config_var('EXT_SUFFIX') or get_config_var('SO'))", + "print(get_config_var('EXT_SUFFIX'))", "print(get_config_var('INCLUDEPY'))", "print(get_config_var('LDLIBRARY'))", "print(get_config_var('prefix'))", From 3322a5a6322700eca3dee1718e311fe5bddf7a72 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 15:08:48 -0400 Subject: [PATCH 08/14] [Cython] Scope hypot redefinition The bug was fixed in Python 3.7.3, so it no longer required. --- interfaces/cython/SConscript | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/SConscript b/interfaces/cython/SConscript index 1477d32b2dd..f1226311e07 100644 --- a/interfaces/cython/SConscript +++ b/interfaces/cython/SConscript @@ -60,8 +60,10 @@ elif localenv['OS'] == 'Windows': localenv.Append(LIBS='python{}'.format(py_version.replace('.',''))) if localenv['OS_BITS'] == 64: localenv.Append(CPPDEFINES='MS_WIN64') - # fix for http://bugs.python.org/issue11566 - localenv.Append(CPPDEFINES={'_hypot':'hypot'}) + # fix for https://bugs.python.org/issue11566. Fixed in 3.7.3 and higher. + # See https://github.com/python/cpython/pull/11283 + if parse_version(py_version) < 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]) From 0663287f3bc24e0a4e8f06acb58d8258adb34d53 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 15:14:16 -0400 Subject: [PATCH 09/14] [Cython] Use setup.cfg to configure the package This change mirrors the one for the source distribution. This change results in a non-executable template file (setup.cfg.in), which makes edits easier. This change removes the requirement to template the Python extension into setup.py, and adds a Setuptools extension module. This results in correct builds when bdist_wheel is specified and uses package_data properly. --- SConstruct | 4 +- interfaces/cython/.gitignore | 8 +-- interfaces/cython/SConscript | 11 ++-- interfaces/cython/setup.cfg.in | 69 ++++++++++++++++++++++ interfaces/cython/setup.py | 7 +++ interfaces/cython/setup.py.in | 103 --------------------------------- 6 files changed, 89 insertions(+), 113 deletions(-) create mode 100644 interfaces/cython/setup.cfg.in create mode 100644 interfaces/cython/setup.py delete mode 100644 interfaces/cython/setup.py.in diff --git a/SConstruct b/SConstruct index 5354852fb86..8cb63022841 100644 --- a/SConstruct +++ b/SConstruct @@ -107,7 +107,9 @@ if "clean" in COMMAND_LINE_TARGETS: remove_directory("include/cantera/ext") remove_file("interfaces/cython/cantera/_cantera.cpp") remove_file("interfaces/cython/cantera/_cantera.h") - remove_file("interfaces/cython/setup.py") + remove_file("interfaces/cython/setup.cfg") + remove_file("interfaces/cython/LICENSE.txt") + remove_file("interfaces/cython/README.rst") remove_file("interfaces/python_minimal/setup.py") remove_file("config.log") remove_directory("doc/sphinx/matlab/examples") diff --git a/interfaces/cython/.gitignore b/interfaces/cython/.gitignore index c76ec86cfad..2a5acec62db 100644 --- a/interfaces/cython/.gitignore +++ b/interfaces/cython/.gitignore @@ -2,10 +2,8 @@ cantera/*.cpp cantera/*.c cantera/data cantera/test/data -setup.py -scripts/ctml_writer.py -scripts/ctml_writer -scripts/ck2cti.py -scripts/ck2cti Cantera.egg-info dist +setup.cfg +LICENSE.txt +README.rst diff --git a/interfaces/cython/SConscript b/interfaces/cython/SConscript index f1226311e07..30ff12d1711 100644 --- a/interfaces/cython/SConscript +++ b/interfaces/cython/SConscript @@ -75,16 +75,19 @@ ext = localenv.LoadableModule('#build/python/cantera/_cantera{}'.format(module_e SHLIBPREFIX='', LIBSUFFIXES=[module_ext]) localenv['py_extension'] = ext[0].name -localenv.SubstFile('setup.py', 'setup.py.in') +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_cmd = ('cd interfaces/cython &&' ' $python_cmd_esc setup.py build --build-lib=../../build/python') -mod = build(localenv.Command('#build/python/cantera/__init__.py', 'setup.py', +mod = build(localenv.Command("#build/python/cantera/__init__.py", "setup.cfg", build_cmd)) env['python_module'] = mod env['python_extension'] = ext -localenv.Depends(mod, ext) -localenv.Depends(mod, dataFiles + testFiles) +localenv.Depends(mod, [ext, dataFiles, testFiles, setup_cfg, readme, license]) localenv.Depends(ext, localenv['cantera_staticlib']) for f in (multi_glob(localenv, 'cantera', 'py') + diff --git a/interfaces/cython/setup.cfg.in b/interfaces/cython/setup.cfg.in new file mode 100644 index 00000000000..ccf42785c0c --- /dev/null +++ b/interfaces/cython/setup.cfg.in @@ -0,0 +1,69 @@ +[metadata] +name = Cantera +version = @cantera_version@ +description = Cantera is an open-source suite of tools for problems involving chemical kinetics, thermodynamics, and transport processes. +long_description = file: README.rst +long_description_content_type = text/x-rst +license_files = LICENSE.txt +url = https://cantera.org +author = Cantera Developers +author_email = developers@cantera.org +keywords = chemistry physics +license = BSD 3-Clause License +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Education + Intended Audience :: Science/Research + License :: OSI Approved :: BSD License + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX :: Linux + Programming Language :: C + Programming Language :: C++ + Programming Language :: Cython + Programming Language :: Fortran + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + Topic :: Scientific/Engineering :: Chemistry + Topic :: Scientific/Engineering :: Physics +project_urls = + Documentation = https://cantera.org/documentation + Funding = https://numfocus.org/donate-to-cantera + Source = https://github.com/Cantera/cantera + Tracker = https://github.com/Cantera/cantera/issues + +[options] +zip_safe = False +include_package_data = True +install_requires = + numpy >= 1.12.0 + ruamel.yaml >= 0.15.34 +python_requires = >=@py_min_ver_str@ +packages = + cantera + cantera.data + cantera.test + cantera.test.data + cantera.examples + +[options.package_data] +cantera.data = *.*, */*.* +cantera.test.data = *.*, */*.* +cantera.examples = */*.* +cantera = *.pxd + +[options.extras_require] +hdf5 = h5py +pandas = pandas + +[options.entry_points] +console_scripts = + ck2cti = cantera.ck2cti:script_entry_point + ctml_writer = cantera.ctml_writer:main + ck2yaml = cantera.ck2yaml:script_entry_point + cti2yaml = cantera.cti2yaml:main + ctml2yaml = cantera.ctml2yaml:main diff --git a/interfaces/cython/setup.py b/interfaces/cython/setup.py new file mode 100644 index 00000000000..a930748fe18 --- /dev/null +++ b/interfaces/cython/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup, Extension + +extension = Extension("cantera._cantera", sources=[]) + +setup( + ext_modules=[extension], +) diff --git a/interfaces/cython/setup.py.in b/interfaces/cython/setup.py.in deleted file mode 100644 index 85e9a01f748..00000000000 --- a/interfaces/cython/setup.py.in +++ /dev/null @@ -1,103 +0,0 @@ -import os -from setuptools import setup - -# Monkey patch to prevent bdist_msi from incorrectly overwriting the value of -# build-lib specified on the command line. -# See http://bugs.python.org/issue1109963 - -# This patch works by returning False the first time that the -# 'has_ext_modules' method is called in bdist_msi.run, which is where the -# replacement of build_lib happens. Subsequent calls to 'has_ext_modules' -# should use the correct value so that the resulting installer is specific to -# this Python version. Known to affect Python versions 2.6 through 3.7. If -# this bug is ever fixed, this patch should be made conditional on the Python -# version. -if os.name == 'nt': - from distutils.command.bdist_msi import bdist_msi - bdist_run_orig = bdist_msi.run - def bdist_run_new(self): - has_ext_modules_orig = self.distribution.has_ext_modules - self._first_call = True - def has_ext_modules(): - if self._first_call: - self._first_call = False - return False - else: - return has_ext_modules_orig() - - self.distribution.has_ext_modules = has_ext_modules - return bdist_run_orig(self) - - bdist_msi.run = bdist_run_new - -# Monkey patch to resolve https://bugs.python.org/issue34251 -# (Affects Python 3.7.0) -if os.name == 'nt': - import msilib - msilib.Win64 = msilib.AMD64 - -# Copy the long_description from docs/sphinx/index.rst -long_description = """ -Cantera is a suite of object-oriented software tools for problems involving -chemical kinetics, thermodynamics, and/or transport processes. - -Cantera provides types (or classes) of objects representing phases of -matter, interfaces between these phases, reaction managers, time-dependent -reactor networks, and steady one-dimensional reacting flows. Cantera is -currently used for applications including combustion, detonations, -electrochemical energy conversion and storage, fuel cells, batteries, aqueous -electrolyte solutions, plasmas, and thin film deposition. - -Cantera can be used from Python and Matlab, or in applications written -in C++ and Fortran 90. -""" - -setup( - name="Cantera", - version="@cantera_version@", - description="The Cantera Python Interface", - long_description=long_description, - author="Raymond Speth", - author_email="speth@mit.edu", - url="https://cantera.org", - packages=[ - 'cantera', - 'cantera.data', - 'cantera.test', - 'cantera.test.data', - 'cantera.examples', - ], - entry_points={ - 'console_scripts': [ - 'ck2cti=cantera.ck2cti:script_entry_point', - 'ctml_writer=cantera.ctml_writer:main', - 'ck2yaml=cantera.ck2yaml:script_entry_point', - 'cti2yaml=cantera.cti2yaml:main', - 'ctml2yaml=cantera.ctml2yaml:main', - ], - }, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: C++', - 'Programming Language :: Fortran', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Topic :: Scientific/Engineering :: Chemistry', - ], - package_data={ - 'cantera.data': ['*.*', '*/*.*'], - 'cantera.test.data': ['*.*', '*/*.*'], - 'cantera.examples': ['*/*.*'], - 'cantera': ["@py_extension@", '*.pxd'], - }, - zip_safe=False, - python_requires=">=@py_min_ver_str@", -) From 5e6875284c505726df6e9d6b7543e7f0478adc44 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 15:36:57 -0400 Subject: [PATCH 10/14] [Python Minimal] Use setup.cfg configuration This change provides similar benefits as for the other two Python distributions. It should allow easier uploading to PyPI and now includes the README and License files. --- SConstruct | 4 ++- interfaces/python_minimal/.gitignore | 5 +-- interfaces/python_minimal/SConscript | 23 +++++++----- interfaces/python_minimal/setup.cfg.in | 49 ++++++++++++++++++++++++++ interfaces/python_minimal/setup.py | 3 ++ interfaces/python_minimal/setup.py.in | 20 ----------- 6 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 interfaces/python_minimal/setup.cfg.in create mode 100644 interfaces/python_minimal/setup.py delete mode 100644 interfaces/python_minimal/setup.py.in diff --git a/SConstruct b/SConstruct index 8cb63022841..3eb0fd07f1e 100644 --- a/SConstruct +++ b/SConstruct @@ -110,7 +110,9 @@ if "clean" in COMMAND_LINE_TARGETS: remove_file("interfaces/cython/setup.cfg") remove_file("interfaces/cython/LICENSE.txt") remove_file("interfaces/cython/README.rst") - remove_file("interfaces/python_minimal/setup.py") + remove_file("interfaces/python_minimal/setup.cfg") + remove_file("interfaces/python_minimal/LICENSE.txt") + remove_file("interfaces/python_minimal/README.rst") remove_file("config.log") remove_directory("doc/sphinx/matlab/examples") remove_file("doc/sphinx/matlab/examples.rst") diff --git a/interfaces/python_minimal/.gitignore b/interfaces/python_minimal/.gitignore index 5ca4392ca92..8a8b5c92556 100644 --- a/interfaces/python_minimal/.gitignore +++ b/interfaces/python_minimal/.gitignore @@ -1,5 +1,3 @@ -setup*.py -scripts/* cantera/ck2cti.py cantera/ctml_writer.py cantera/ck2yaml.py @@ -8,3 +6,6 @@ cantera/ctml2yaml.py build dist Cantera_minimal.egg-info +LICENSE.txt +README.rst +setup.cfg diff --git a/interfaces/python_minimal/SConscript b/interfaces/python_minimal/SConscript index 9c3ab3e78c3..2047a06ef97 100644 --- a/interfaces/python_minimal/SConscript +++ b/interfaces/python_minimal/SConscript @@ -7,23 +7,30 @@ Import('env', 'build', 'install') localenv = env.Clone() -make_setup = build(localenv.SubstFile('setup.py', 'setup.py.in')) +make_setup = build(localenv.SubstFile("setup.cfg", "setup.cfg.in")) # copy scripts from the full Cython module -for script in ['ctml_writer', 'ck2cti', 'ck2yaml', 'cti2yaml', 'ctml2yaml']: +for script in ["ctml_writer", "ck2cti", "ck2yaml", "cti2yaml", "ctml2yaml"]: # The actual script - s = build(env.Command('cantera/{}.py'.format(script), - '#interfaces/cython/cantera/{}.py'.format(script), - Copy('$TARGET', '$SOURCE'))) + s = build(env.Command(f"cantera/{script}.py", + f"#interfaces/cython/cantera/{script}.py", + Copy("$TARGET", "$SOURCE"))) localenv.Depends(make_setup, s) -build_cmd = ('cd interfaces/python_minimal &&' - ' $python_cmd_esc setup.py build --build-lib=../../build/python') +build_cmd = ("cd interfaces/python_minimal && " + "$python_cmd_esc setup.py build --build-base=../../build/python") -mod = build(localenv.Command('#build/python/cantera/__init__.py', 'setup.py', +mod = build(localenv.Command("#build/python/cantera/__init__.py", "setup.cfg", build_cmd)) env['python_module'] = mod +readme = localenv.Command("README.rst", "#README.rst", Copy("$TARGET", "$SOURCE")) +# The target of this command must match the file listed in setup.cfg.in +license = localenv.Command("LICENSE.txt", "#License.txt", + Copy("$TARGET", "$SOURCE")) +localenv.Depends(license, localenv["license_target"]) +localenv.Depends(mod, [make_setup, readme, license]) + if localenv['PYTHON_INSTALLER'] == 'direct': if localenv['python_prefix'] == 'USER': # Install to the OS-dependent user site-packages directory diff --git a/interfaces/python_minimal/setup.cfg.in b/interfaces/python_minimal/setup.cfg.in new file mode 100644 index 00000000000..d656b872206 --- /dev/null +++ b/interfaces/python_minimal/setup.cfg.in @@ -0,0 +1,49 @@ +[metadata] +name = Cantera_minimal +version = @cantera_version@ +description = A minimal Cantera interface, containing only the input file conversion scripts +long_description = This Cantera interface contains the conversion scripts for input files into the Cantera format. For the full Python interface, please see [Cantera](https://pypi.org/project/cantera). +long_description_content_type = text/markdown +license_files = LICENSE.txt +url = https://cantera.org +author = Cantera Developers +author_email = developers@cantera.org +keywords = chemistry physics +license = BSD 3-Clause License +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Education + Intended Audience :: Science/Research + License :: OSI Approved :: BSD License + Operating System :: MacOS :: MacOS X + Operating System :: Microsoft :: Windows + Operating System :: POSIX :: Linux + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Topic :: Scientific/Engineering :: Chemistry + Topic :: Scientific/Engineering :: Physics +project_urls = + Documentation = https://cantera.org/documentation + Funding = https://numfocus.org/donate-to-cantera + Source = https://github.com/Cantera/cantera + Tracker = https://github.com/Cantera/cantera/issues + +[options] +zip_safe = True +install_requires = + numpy >= 1.12.0 + ruamel.yaml >= 0.15.34 +python_requires = >=@py_min_ver_str@ +packages = + cantera + +[options.entry_points] +console_scripts = + ck2cti = cantera.ck2cti:script_entry_point + ctml_writer = cantera.ctml_writer:main + ck2yaml = cantera.ck2yaml:script_entry_point + cti2yaml = cantera.cti2yaml:main + ctml2yaml = cantera.ctml2yaml:main diff --git a/interfaces/python_minimal/setup.py b/interfaces/python_minimal/setup.py new file mode 100644 index 00000000000..606849326a4 --- /dev/null +++ b/interfaces/python_minimal/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/interfaces/python_minimal/setup.py.in b/interfaces/python_minimal/setup.py.in deleted file mode 100644 index ead7760440a..00000000000 --- a/interfaces/python_minimal/setup.py.in +++ /dev/null @@ -1,20 +0,0 @@ -from setuptools import setup - -setup(name="Cantera_minimal", - version="@cantera_version@", - description="The Minimal Cantera Python Interface", - long_description="", - author="Raymond Speth", - author_email="speth@mit.edu", - url="http://www.cantera.org", - packages = ['cantera'], - entry_points={ - 'console_scripts': [ - 'ck2cti=cantera.ck2cti:script_entry_point', - 'ctml_writer=cantera.ctml_writer:main', - 'ck2yaml=cantera.ck2yaml:script_entry_point', - 'cti2yaml=cantera.cti2yaml:main', - 'ctml2yaml=cantera.ctml2yaml:main', - ], - }, -) From 70d308409943b9d68075165e3216e8b4130f2676 Mon Sep 17 00:00:00 2001 From: Bryan Weber Date: Fri, 16 Jul 2021 18:57:20 -0400 Subject: [PATCH 11/14] [CI] Relax setuptools requirement on Windows --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a007f3ab765..7898fd57b3b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -344,7 +344,7 @@ jobs: architecture: x64 - name: Install Python dependencies run: | - python -m pip install -U pip 'setuptools>=47.0.0,<48' + python -m pip install -U pip 'setuptools>=43.0.0' python -m pip install scons pypiwin32 numpy ruamel.yaml cython h5py pandas pytest pytest-github-actions-annotate-failures - name: Restore Boost cache uses: actions/cache@v2 From 4e46072b5edba86515961e2a65860e576020b538 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 17 Jul 2021 09:42:01 -0400 Subject: [PATCH 12/14] [Cython] Fix Windows builds For some versions of Python, the DLL module extension included in the sysconfig module is not correct. In that case, the file extension needs to be constructed manually. To make this information easier to get to, the JSON module is used to parse data returned by sysconfig from the Python which will be building the module. --- interfaces/cython/SConscript | 46 +++++++++++++++++++++++++----------- interfaces/cython/setup.py | 4 +--- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/interfaces/cython/SConscript b/interfaces/cython/SConscript index 30ff12d1711..068abf87d92 100644 --- a/interfaces/cython/SConscript +++ b/interfaces/cython/SConscript @@ -4,6 +4,7 @@ from os.path import join as pjoin from os.path import normpath from pathlib import Path from pkg_resources import parse_version +import json from buildutils import * Import('env', 'build', 'install') @@ -32,24 +33,41 @@ testFiles = localenv.RecursiveInstall('#interfaces/cython/cantera/test/data', build(testFiles) # Get information needed to build the Python module -script = '\n'.join(("from sysconfig import *", - "import numpy", - "print(get_config_var('EXT_SUFFIX'))", - "print(get_config_var('INCLUDEPY'))", - "print(get_config_var('LDLIBRARY'))", - "print(get_config_var('prefix'))", - "print(get_python_version())", - "print(numpy.get_include())")) -info = get_command_output(localenv["python_cmd"], "-c", script) -module_ext, inc, pylib, prefix, py_version, numpy_include = info.splitlines()[-6:] +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"]) +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{info['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 parse_version(py_version) == parse_version('3.8'): +if localenv["HAS_CLANG"] and py_version_short == parse_version("3.8"): localenv.Append(CXXFLAGS='-Wno-deprecated-declarations') if localenv['OS'] == 'Darwin': @@ -57,12 +75,12 @@ if localenv['OS'] == 'Darwin': elif localenv['OS'] == 'Windows': localenv.Append(LIBPATH=prefix+'/libs') if localenv['toolchain'] == 'mingw': - localenv.Append(LIBS='python{}'.format(py_version.replace('.',''))) + localenv.Append(LIBS=f"python{info['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. + # Fix for https://bugs.python.org/issue11566. Fixed in 3.7.3 and higher. # See https://github.com/python/cpython/pull/11283 - if parse_version(py_version) < parse_version("3.7.3"): + 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' diff --git a/interfaces/cython/setup.py b/interfaces/cython/setup.py index a930748fe18..667310b861a 100644 --- a/interfaces/cython/setup.py +++ b/interfaces/cython/setup.py @@ -2,6 +2,4 @@ extension = Extension("cantera._cantera", sources=[]) -setup( - ext_modules=[extension], -) +setup(ext_modules=[extension]) From 0f7a8b2f3e316964bc40dc4acd8d407d3f0c20a1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 21 Nov 2021 20:22:55 -0500 Subject: [PATCH 13/14] Update packaging workflow --- .github/workflows/python-package.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 73aa2968fcb..508faf08d68 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -25,16 +25,13 @@ jobs: - name: Update pip run: python3 -m pip install -U pip setuptools - name: Install dependencies - run: python3 -m pip install scons numpy cython build + run: python3 -m pip install scons build - name: Build the interface - run: python3 `which scons` build f90_interface=n debug=n googletest=none python_package=sdist use_pch=n - - name: Build the sdist - run: python3 -m build -s - working-directory: interfaces/python_sdist + run: python3 `which scons` sdist f90_interface=n python_package='none' - name: Archive the built sdist uses: actions/upload-artifact@v2 with: - path: ./interfaces/python_sdist/dist/*.tar.gz + path: ./build/python_sdist/dist/*.tar.gz name: sdist if-no-files-found: error @@ -44,7 +41,7 @@ jobs: needs: ["sdist"] strategy: matrix: - py: ["36", "37", "38", "39"] + py: ["36", "37", "38", "39", "310"] arch: ["x86_64", "i686", "aarch64", "ppc64le", "s390x"] fail-fast: true env: @@ -76,7 +73,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v1.12.0 + uses: pypa/cibuildwheel@v2.2.2 env: CIBW_BUILD: cp${{ matrix.py }}-* CIBW_ARCHS: ${{ matrix.arch }} @@ -93,7 +90,7 @@ jobs: needs: ["sdist"] strategy: matrix: - py: ["36", "37", "38", "39"] + py: ["36", "37", "38", "39", "310"] arch: ["AMD64", "x86"] fail-fast: true env: @@ -124,7 +121,7 @@ jobs: rm $BOOST_ROOT/download.7z shell: bash - name: Build wheels - uses: pypa/cibuildwheel@v1.12.0 + uses: pypa/cibuildwheel@v2.2.2 env: CIBW_ENVIRONMENT: "BOOST_INCLUDE=${BOOST_ROOT}" CIBW_ARCHS: ${{ matrix.arch }} @@ -144,7 +141,7 @@ jobs: MACOSX_DEPLOYMENT_TARGET: 10.9 strategy: matrix: - py: ["36", "37", "38", "39"] + py: ["36", "37", "38", "39", "310"] fail-fast: true steps: - name: Download pre-built sdist @@ -156,7 +153,7 @@ jobs: - name: Install Brew dependencies run: brew install boost - name: Build wheels - uses: pypa/cibuildwheel@v1.12.0 + uses: pypa/cibuildwheel@v2.2.2 env: CIBW_BUILD: cp${{ matrix.py }}-* CIBW_SKIP: cp35-* @@ -174,7 +171,7 @@ jobs: needs: ["sdist"] strategy: matrix: - py: ["38", "39"] + py: ["38", "39", "310"] fail-fast: true env: SDKROOT: "macosx11.0" @@ -192,7 +189,7 @@ jobs: - name: Install Brew dependencies run: brew install boost - name: Build wheels - uses: pypa/cibuildwheel@v1.12.0 + uses: pypa/cibuildwheel@v2.2.2 env: CIBW_ENVIRONMENT: 'BOOST_INCLUDE="$(brew --prefix)/include"' CIBW_ARCHS: "arm64" From a122a515d985075af713ba216bedb12adcce9aa1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 22 Nov 2021 07:13:01 -0500 Subject: [PATCH 14/14] Switch to build frontend --- .github/workflows/python-package.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 508faf08d68..2980dc79794 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -5,6 +5,10 @@ on: branches: - add-pypi-package +env: + CIBW_BUILD_FRONTEND: build + CIBW_BUILD_VERBOSITY: 3 + jobs: sdist: name: Build the sdist